From b3973de2a44c8e514dc3e4d9b96edefbeecd037a Mon Sep 17 00:00:00 2001 From: han4wluc Date: Sun, 12 Apr 2026 00:49:14 +0800 Subject: [PATCH] Improve layout editor preview states and default styles --- bun.lock | 4 +- package.json | 2 +- .../commandLineRuntimeAction.schema.yaml | 10 +- .../layoutEditorCanvas.handlers.js | 1 + .../support/layoutEditorCanvasRender.js | 68 ++++- .../layoutEditorPreview.handlers.js | 100 ++++++++ .../layoutEditorPreview.store.js | 232 +++++++++++++++++- .../layoutEditorPreview.view.yaml | 111 ++++++++- .../support/layoutEditorPreviewData.js | 5 + src/internal/project/projection.js | 3 +- src/pages/layoutEditor/layoutEditor.view.yaml | 5 +- static/templates/default/repository.json | 51 +++- .../commandLineRuntimeAction.schema.test.js | 29 +++ tests/project/projection.test.js | 144 +++++++++++ 14 files changed, 736 insertions(+), 29 deletions(-) create mode 100644 tests/commandLineRuntimeAction/commandLineRuntimeAction.schema.test.js diff --git a/bun.lock b/bun.lock index 1f8988aa..e94678f0 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,7 @@ "jszip": "^3.10.1", "nanoid": "^5.1.5", "route-engine-js": "1.0.0", - "route-graphics": "1.7.1", + "route-graphics": "1.7.2", "rxjs": "^7.8.2", "snabbdom": "^3.6.2", "snabbdom-to-html": "^7.1.0", @@ -689,7 +689,7 @@ "route-engine-js": ["route-engine-js@1.0.0", "", { "dependencies": { "immer": "^10.1.1", "jempl": "1.0.0", "js-yaml": "^4.1.0", "puty": "^0.1.1" } }, "sha512-I8Z3m7yM5MHzKrNAe7S3Zu4WMvw0pVfyfQZGhftmhkCg/6hnEtlgwJb0mb0Ex7EjSya+D4P8hk3211lI2BpZAg=="], - "route-graphics": ["route-graphics@1.7.1", "", { "dependencies": { "@pixi/unsafe-eval": "^7.4.3", "hotkeys-js": "^4.0.0-beta.7", "pixi.js": "^8.7.1" } }, "sha512-U9ZgQW9oduMzbMORse6gdDm/LdU5RoIrbp9pI0vPkxpvG5rnUYoN83MjtLHvF2C9FlsvOa1MjU7jqtNDwfc3jA=="], + "route-graphics": ["route-graphics@1.7.2", "", { "dependencies": { "@pixi/unsafe-eval": "^7.4.3", "hotkeys-js": "^4.0.0-beta.7", "pixi.js": "^8.7.1" } }, "sha512-HeFufRjZwPiR02wPeqlfPtDowzcBAwwOhHohfFXnUcnEOkGv8gSbam/q67poftX767RcxBpyN+jhx9zBXkA+rw=="], "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], diff --git a/package.json b/package.json index a73d60da..3441dad2 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "jszip": "^3.10.1", "nanoid": "^5.1.5", "route-engine-js": "1.0.0", - "route-graphics": "1.7.1", + "route-graphics": "1.7.2", "rxjs": "^7.8.2", "snabbdom": "^3.6.2", "snabbdom-to-html": "^7.1.0", diff --git a/src/components/commandLineRuntimeAction/commandLineRuntimeAction.schema.yaml b/src/components/commandLineRuntimeAction/commandLineRuntimeAction.schema.yaml index 762c37d8..bac41d83 100644 --- a/src/components/commandLineRuntimeAction/commandLineRuntimeAction.schema.yaml +++ b/src/components/commandLineRuntimeAction/commandLineRuntimeAction.schema.yaml @@ -1,7 +1,9 @@ componentName: rvn-command-line-runtime-action propsSchema: - mode: - type: string - action: - type: object + type: object + properties: + mode: + type: string + action: + type: object diff --git a/src/components/layoutEditorCanvas/layoutEditorCanvas.handlers.js b/src/components/layoutEditorCanvas/layoutEditorCanvas.handlers.js index 0a30652b..59ac589a 100644 --- a/src/components/layoutEditorCanvas/layoutEditorCanvas.handlers.js +++ b/src/components/layoutEditorCanvas/layoutEditorCanvas.handlers.js @@ -322,6 +322,7 @@ const renderLayoutEditorCanvas = async ( layoutState, repositoryState, previewData: props.previewData, + resolution: props.resolution, selectedItemId: props.selectedItemId, disableMoveDrag: props.disableMoveDrag === true, graphicsService: deps.graphicsService, diff --git a/src/components/layoutEditorCanvas/support/layoutEditorCanvasRender.js b/src/components/layoutEditorCanvas/support/layoutEditorCanvasRender.js index c20c2c46..eb0aebd2 100644 --- a/src/components/layoutEditorCanvas/support/layoutEditorCanvasRender.js +++ b/src/components/layoutEditorCanvas/support/layoutEditorCanvasRender.js @@ -93,6 +93,11 @@ const normalizeLayoutEditorPreviewData = (previewData = {}) => { return { ...nextPreviewData, + backgroundImageId: + typeof nextPreviewData.backgroundImageId === "string" && + nextPreviewData.backgroundImageId.length > 0 + ? nextPreviewData.backgroundImageId + : undefined, variables: toPlainObject(nextPreviewData.variables), runtime: { ...nextRuntime, @@ -154,6 +159,51 @@ const normalizeLayoutEditorPreviewData = (previewData = {}) => { }; }; +const toPositiveNumber = (value, fallback) => { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +}; + +const createLayoutEditorPreviewBackgroundElement = ({ + previewData, + repositoryState, + resolution, +} = {}) => { + const backgroundImageId = previewData?.backgroundImageId; + if (!backgroundImageId) { + return undefined; + } + + const imageItem = repositoryState?.images?.items?.[backgroundImageId]; + if (imageItem?.type && imageItem.type !== "image") { + return undefined; + } + + const fileId = imageItem?.fileId; + if (typeof fileId !== "string" || fileId.length === 0) { + return undefined; + } + + const resolutionWidth = toPositiveNumber(resolution?.width, undefined); + const resolutionHeight = toPositiveNumber(resolution?.height, undefined); + if (resolutionWidth === undefined || resolutionHeight === undefined) { + return undefined; + } + + return { + id: "layout-editor-preview-background", + type: "sprite", + src: fileId, + fileType: imageItem?.fileType ?? "image/png", + x: Math.round(resolutionWidth / 2), + y: Math.round(resolutionHeight / 2), + width: toPositiveNumber(imageItem?.width, resolutionWidth), + height: toPositiveNumber(imageItem?.height, resolutionHeight), + anchorX: 0.5, + anchorY: 0.5, + }; +}; + const isSelectableMatch = (elementId, selectedItemId) => { if (typeof elementId !== "string" || typeof selectedItemId !== "string") { return false; @@ -579,6 +629,7 @@ export const createLayoutEditorRenderedElements = ({ layoutState, repositoryState, previewData, + resolution, selectedItemId, disableMoveDrag, graphicsService, @@ -587,15 +638,24 @@ export const createLayoutEditorRenderedElements = ({ layoutState, repositoryState, }); + const normalizedPreviewData = normalizeLayoutEditorPreviewData(previewData); const finalElements = resolveLayoutPreviewElements({ elements: renderStateElements, - previewData, + previewData: normalizedPreviewData, }); const resolvedFinalElements = resolveLayoutReferences(finalElements, { resources, }); + const previewBackgroundElement = createLayoutEditorPreviewBackgroundElement({ + previewData: normalizedPreviewData, + repositoryState, + resolution, + }); + const renderedElements = previewBackgroundElement + ? [previewBackgroundElement, ...resolvedFinalElements] + : resolvedFinalElements; const parsedState = graphicsService.parse({ - elements: resolvedFinalElements, + elements: renderedElements, }); const overlayElements = createLayoutEditorSelectionOverlay({ parsedElements: parsedState.elements, @@ -608,8 +668,8 @@ export const createLayoutEditorRenderedElements = ({ ); return { - elements: [...resolvedFinalElements, ...overlayElements], - fileReferences: extractFileIdsFromRenderState(resolvedFinalElements), + elements: [...renderedElements, ...overlayElements], + fileReferences: extractFileIdsFromRenderState(renderedElements), selectedElementMetrics, }; }; diff --git a/src/components/layoutEditorPreview/layoutEditorPreview.handlers.js b/src/components/layoutEditorPreview/layoutEditorPreview.handlers.js index 9bd09da8..e3cc7c74 100644 --- a/src/components/layoutEditorPreview/layoutEditorPreview.handlers.js +++ b/src/components/layoutEditorPreview/layoutEditorPreview.handlers.js @@ -132,6 +132,106 @@ export const handlePreviewRevealingSpeedInput = (deps, payload) => { renderAndEmitPreviewDataChange(deps); }; +export const handlePreviewBackgroundFieldClick = async (deps) => { + await syncRepositoryState(deps); + deps.store.hideDropdownMenu(); + deps.store.openImageSelectorDialog(); + deps.render(); +}; + +export const handlePreviewBackgroundFieldContextMenu = (deps, payload) => { + const imageId = deps.store.selectPreviewData()?.backgroundImageId; + if (!imageId) { + return; + } + + const event = payload._event; + event.preventDefault(); + deps.store.showDropdownMenu({ + x: event.clientX, + y: event.clientY, + }); + deps.render(); +}; + +export const handleImageSelected = (deps, payload) => { + deps.store.setImageSelectorSelectedImageId({ + imageId: payload._event.detail?.imageId, + }); + deps.render(); +}; + +export const handleImageSelectorCancel = (deps) => { + deps.store.closeImageSelectorDialog(); + deps.render(); +}; + +export const handleImageSelectorSubmit = (deps) => { + const state = deps.store.getState + ? deps.store.getState() + : deps.store._state || deps.store.state; + const imageSelectorDialog = state?.imageSelectorDialog; + + deps.store.setPreviewBackgroundImageId({ + imageId: imageSelectorDialog?.selectedImageId, + }); + deps.store.closeImageSelectorDialog(); + deps.store.hideDropdownMenu(); + renderAndEmitPreviewDataChange(deps); +}; + +export const handleImageDoubleClick = (deps, payload) => { + const imageId = payload?._event?.detail?.imageId; + if (!imageId) { + return; + } + + deps.store.showFullImagePreview({ imageId }); + deps.render(); +}; + +export const handleFileExplorerClickItem = (deps, payload) => { + const itemId = payload?._event?.detail?.itemId; + if (!itemId) { + return; + } + + deps.refs.imageSelector?.transformedHandlers?.handleScrollToItem?.({ + itemId, + }); +}; + +export const handleClearPreviewBackground = (deps) => { + deps.store.setPreviewBackgroundImageId({ + imageId: undefined, + }); + deps.store.hideDropdownMenu(); + renderAndEmitPreviewDataChange(deps); +}; + +export const handleDropdownMenuClickItem = (deps, payload) => { + const item = payload._event.detail?.item || payload._event.detail; + + deps.store.hideDropdownMenu(); + + if (item?.value === "remove-background") { + handleClearPreviewBackground(deps); + return; + } + + deps.render(); +}; + +export const handleDropdownMenuClose = (deps) => { + deps.store.hideDropdownMenu(); + deps.render(); +}; + +export const handlePreviewOverlayClick = (deps) => { + deps.store.hideFullImagePreview(); + deps.render(); +}; + export const handlePlayPreviewClick = (deps) => { emitPlay(deps); }; diff --git a/src/components/layoutEditorPreview/layoutEditorPreview.store.js b/src/components/layoutEditorPreview/layoutEditorPreview.store.js index 1d7f547c..b718e64e 100644 --- a/src/components/layoutEditorPreview/layoutEditorPreview.store.js +++ b/src/components/layoutEditorPreview/layoutEditorPreview.store.js @@ -8,11 +8,25 @@ import { findSaveLoadPreviewSettings, getSaveLoadPreviewWindow, } from "./support/layoutEditorPreviewSupport.js"; +import { toFlatItems } from "../../internal/project/tree.js"; const EMPTY_LAYOUT_DATA = { items: {}, tree: [], }; +const PREVIEW_BACKGROUND_SLOT = "preview-background"; +const PREVIEW_BACKGROUND_FORM_FIELD = { + type: "slot", + slot: PREVIEW_BACKGROUND_SLOT, + label: "Background", +}; +const PREVIEW_BACKGROUND_MENU_ITEMS = [ + { + type: "item", + label: "Remove Background", + value: "remove-background", + }, +]; const createDialogueDefaultValues = () => ({ "dialogue-character-name": "Character", @@ -61,6 +75,104 @@ const resetPreviewStateValues = (state) => { state.historyDefaultValues = createHistoryDefaultValues(); state.saveLoadDefaultValues = createSaveLoadDefaultValues(); state.previewVariableValues = {}; + state.previewBackgroundImageId = undefined; + state.imageSelectorDialog = { + open: false, + selectedImageId: undefined, + }; + state.dropdownMenu = { + isOpen: false, + x: 0, + y: 0, + items: [], + }; + state.fullImagePreviewVisible = false; + state.fullImagePreviewImageId = undefined; +}; + +const cloneFormField = (field) => { + if (!field || typeof field !== "object") { + return field; + } + + const nextField = { + ...field, + }; + + if (Array.isArray(field.fields)) { + nextField.fields = field.fields.map((childField) => + cloneFormField(childField), + ); + } + + return nextField; +}; + +const withPreviewBackgroundSlot = (form) => { + if (!form || typeof form !== "object") { + return form; + } + + const nextFields = Array.isArray(form.fields) + ? form.fields.map((field) => cloneFormField(field)) + : []; + + if ( + nextFields.some( + (field) => + field?.type === "slot" && field?.slot === PREVIEW_BACKGROUND_SLOT, + ) + ) { + return { + ...form, + fields: nextFields, + }; + } + + return { + ...form, + fields: [PREVIEW_BACKGROUND_FORM_FIELD, ...nextFields], + }; +}; + +const createPreviewBackgroundOnlyForm = () => { + return withPreviewBackgroundSlot({ + title: "Preview", + description: "Choose preview-only data for the canvas", + fields: [], + }); +}; + +const resolvePreviewBackgroundFormTarget = ({ + layoutType, + hasPreviewVariables, + hasSaveLoadPreview, +} = {}) => { + if (layoutType === "dialogue") { + return "dialogue"; + } + + if (layoutType === "nvl") { + return "nvl"; + } + + if (layoutType === "choice") { + return "choice"; + } + + if (layoutType === "history") { + return "history"; + } + + if (hasPreviewVariables) { + return "previewVariables"; + } + + if (hasSaveLoadPreview) { + return "saveLoad"; + } + + return "backgroundOnly"; }; const getLayoutState = (state) => { @@ -83,6 +195,19 @@ export const createInitialState = () => ({ historyDefaultValues: createHistoryDefaultValues(), saveLoadDefaultValues: createSaveLoadDefaultValues(), previewVariableValues: {}, + previewBackgroundImageId: undefined, + imageSelectorDialog: { + open: false, + selectedImageId: undefined, + }, + dropdownMenu: { + isOpen: false, + x: 0, + y: 0, + items: [], + }, + fullImagePreviewVisible: false, + fullImagePreviewImageId: undefined, }); export const setLayoutState = ({ state }, { layoutState } = {}) => { @@ -262,6 +387,57 @@ export const setPreviewVariableValue = ( state.previewVariableValues[name] = fieldValue; }; +export const setPreviewBackgroundImageId = ({ state }, { imageId } = {}) => { + state.previewBackgroundImageId = imageId ?? undefined; +}; + +export const openImageSelectorDialog = ({ state }, _payload = {}) => { + state.imageSelectorDialog.open = true; + state.imageSelectorDialog.selectedImageId = state.previewBackgroundImageId; + state.dropdownMenu.isOpen = false; + state.dropdownMenu.x = 0; + state.dropdownMenu.y = 0; + state.dropdownMenu.items = []; +}; + +export const closeImageSelectorDialog = ({ state }, _payload = {}) => { + state.imageSelectorDialog.open = false; + state.imageSelectorDialog.selectedImageId = undefined; +}; + +export const setImageSelectorSelectedImageId = ( + { state }, + { imageId } = {}, +) => { + state.imageSelectorDialog.selectedImageId = imageId ?? undefined; +}; + +export const showFullImagePreview = ({ state }, { imageId } = {}) => { + state.fullImagePreviewVisible = true; + state.fullImagePreviewImageId = imageId; +}; + +export const hideFullImagePreview = ({ state }, _payload = {}) => { + state.fullImagePreviewVisible = false; + state.fullImagePreviewImageId = undefined; +}; + +export const showDropdownMenu = ({ state }, { x, y, items } = {}) => { + state.dropdownMenu.isOpen = true; + state.dropdownMenu.x = x ?? 0; + state.dropdownMenu.y = y ?? 0; + state.dropdownMenu.items = Array.isArray(items) + ? items + : PREVIEW_BACKGROUND_MENU_ITEMS; +}; + +export const hideDropdownMenu = ({ state }, _payload = {}) => { + state.dropdownMenu.isOpen = false; + state.dropdownMenu.x = 0; + state.dropdownMenu.y = 0; + state.dropdownMenu.items = []; +}; + export const selectPreviewVariableValues = ({ state }) => { return state.previewVariableValues; }; @@ -331,6 +507,7 @@ export const selectPreviewData = ({ state }) => { choicesData: selectChoicesData({ state }), saveLoadData: selectSaveLoadData({ state }), hasSaveLoadPreview: selectHasSaveLoadPreview({ state }), + backgroundImageId: state.previewBackgroundImageId, }); }; @@ -357,13 +534,42 @@ export const selectViewData = ({ state, constants }) => { saveLoadForm: constants.saveLoadForm, }); const identityKey = `${layoutState.id ?? "none"}:${layoutType ?? "none"}`; + const previewBackgroundFormTarget = resolvePreviewBackgroundFormTarget({ + layoutType, + hasPreviewVariables: previewVariablesViewData.hasPreviewVariables, + hasSaveLoadPreview: saveLoadPreviewViewData.hasSaveLoadPreview, + }); + const fileExplorerItems = toFlatItems( + state.repositoryState.images ?? { + items: {}, + tree: [], + }, + ).filter((item) => item.type === "folder"); return { layoutType, - dialogueForm: constants.dialogueForm, + previewBackgroundSlot: PREVIEW_BACKGROUND_SLOT, + previewBackgroundImageId: state.previewBackgroundImageId, + previewBackgroundOnlyForm: + previewBackgroundFormTarget === "backgroundOnly" + ? createPreviewBackgroundOnlyForm() + : undefined, + previewBackgroundOnlyFormKey: `${identityKey}:background-only`, + imageSelectorDialog: state.imageSelectorDialog, + dropdownMenu: state.dropdownMenu, + fileExplorerItems, + fullImagePreviewVisible: state.fullImagePreviewVisible, + fullImagePreviewImageId: state.fullImagePreviewImageId, + dialogueForm: + previewBackgroundFormTarget === "dialogue" + ? withPreviewBackgroundSlot(constants.dialogueForm) + : constants.dialogueForm, dialogueDefaultValues: state.dialogueDefaultValues, dialogueFormKey: `${identityKey}:dialogue`, - nvlForm: constants.nvlForm, + nvlForm: + previewBackgroundFormTarget === "nvl" + ? withPreviewBackgroundSlot(constants.nvlForm) + : constants.nvlForm, nvlDefaultValues: createNvlFormDefaultValues(state.nvlDefaultValues), nvlContext: { characterNames: state.nvlDefaultValues.characterNames, @@ -372,7 +578,10 @@ export const selectViewData = ({ state, constants }) => { }, nvlFormKey: `${identityKey}:nvl`, previewRevealingSpeed: state.previewRevealingSpeed, - choiceForm: constants.choiceForm, + choiceForm: + previewBackgroundFormTarget === "choice" + ? withPreviewBackgroundSlot(constants.choiceForm) + : constants.choiceForm, choiceDefaultValues: createChoiceFormDefaultValues( state.choiceDefaultValues, ), @@ -381,7 +590,10 @@ export const selectViewData = ({ state, constants }) => { choicesNum: state.choiceDefaultValues.choicesNum, }, choiceFormKey: `${identityKey}:choice`, - historyForm: constants.historyForm, + historyForm: + previewBackgroundFormTarget === "history" + ? withPreviewBackgroundSlot(constants.historyForm) + : constants.historyForm, historyDefaultValues: createHistoryFormDefaultValues( state.historyDefaultValues, ), @@ -391,12 +603,20 @@ export const selectViewData = ({ state, constants }) => { linesNum: state.historyDefaultValues.linesNum, }, historyFormKey: `${identityKey}:history`, - saveLoadForm: saveLoadPreviewViewData.saveLoadForm, + saveLoadForm: + previewBackgroundFormTarget === "saveLoad" + ? withPreviewBackgroundSlot(saveLoadPreviewViewData.saveLoadForm) + : saveLoadPreviewViewData.saveLoadForm, saveLoadDefaultValues: saveLoadPreviewViewData.saveLoadDefaultValues, saveLoadContext: saveLoadPreviewViewData.saveLoadContext, saveLoadFormKey: saveLoadPreviewViewData.saveLoadFormKey, hasSaveLoadPreview: saveLoadPreviewViewData.hasSaveLoadPreview, - previewVariablesForm: previewVariablesViewData.previewVariablesForm, + previewVariablesForm: + previewBackgroundFormTarget === "previewVariables" + ? withPreviewBackgroundSlot( + previewVariablesViewData.previewVariablesForm, + ) + : previewVariablesViewData.previewVariablesForm, previewVariablesDefaultValues: previewVariablesViewData.previewVariablesDefaultValues, previewVariablesFormKey: previewVariablesViewData.previewVariablesFormKey, diff --git a/src/components/layoutEditorPreview/layoutEditorPreview.view.yaml b/src/components/layoutEditorPreview/layoutEditorPreview.view.yaml index f352d51e..90fd4d99 100644 --- a/src/components/layoutEditorPreview/layoutEditorPreview.view.yaml +++ b/src/components/layoutEditorPreview/layoutEditorPreview.view.yaml @@ -1,4 +1,10 @@ refs: + previewBackgroundField: + eventListeners: + click: + handler: handlePreviewBackgroundFieldClick + contextmenu: + handler: handlePreviewBackgroundFieldContextMenu dialogueForm: eventListeners: form-input: @@ -39,25 +45,118 @@ refs: handler: handlePreviewVariablesFormChange form-change: handler: handlePreviewVariablesFormChange + baseFileExplorer: + eventListeners: + item-click: + handler: handleFileExplorerClickItem + imageSelectorDialog: + eventListeners: + close: + handler: handleImageSelectorCancel + imageSelector: + eventListeners: + image-selected: + handler: handleImageSelected + image-dblclick: + handler: handleImageDoubleClick + confirmImageSelection: + eventListeners: + click: + handler: handleImageSelectorSubmit + cancelImageSelection: + eventListeners: + click: + handler: handleImageSelectorCancel + dropdownMenu: + eventListeners: + close: + handler: handleDropdownMenuClose + item-click: + handler: handleDropdownMenuClickItem playPreviewButton: eventListeners: click: handler: handlePlayPreviewClick + previewOverlay: + eventListeners: + click: + handler: handlePreviewOverlayClick template: + - $if previewBackgroundOnlyForm: + - rtgl-form key=${previewBackgroundOnlyFormKey} w=f :form=${previewBackgroundOnlyForm}: + - rtgl-view slot=${previewBackgroundSlot} g=xs: + - rtgl-view#previewBackgroundField w=220 h=160 bw=xs bc=bo br=md cur=pointer av=c ah=c: + - $if previewBackgroundImageId: + - rvn-file-image imageId=${previewBackgroundImageId} source="thumbnail" w=220 h=160 br=md: null + $else: + - rtgl-text c=mu-fg ta=c: Select image - $if layoutType == 'dialogue': - - rtgl-form#dialogueForm key=${dialogueFormKey} w=f :form=${dialogueForm} :defaultValues=${dialogueDefaultValues}: null + - rtgl-form#dialogueForm key=${dialogueFormKey} w=f :form=${dialogueForm} :defaultValues=${dialogueDefaultValues}: + - rtgl-view slot=${previewBackgroundSlot} g=xs: + - rtgl-view#previewBackgroundField w=220 h=160 bw=xs bc=bo br=md cur=pointer av=c ah=c: + - $if previewBackgroundImageId: + - rvn-file-image imageId=${previewBackgroundImageId} source="thumbnail" w=220 h=160 br=md: null + $else: + - rtgl-text c=mu-fg ta=c: Select image - $if layoutType == 'nvl': - - rtgl-form#nvlForm key=${nvlFormKey} w=f :form=${nvlForm} :defaultValues=${nvlDefaultValues} :context=${nvlContext}: null + - rtgl-form#nvlForm key=${nvlFormKey} w=f :form=${nvlForm} :defaultValues=${nvlDefaultValues} :context=${nvlContext}: + - rtgl-view slot=${previewBackgroundSlot} g=xs: + - rtgl-view#previewBackgroundField w=220 h=160 bw=xs bc=bo br=md cur=pointer av=c ah=c: + - $if previewBackgroundImageId: + - rvn-file-image imageId=${previewBackgroundImageId} source="thumbnail" w=220 h=160 br=md: null + $else: + - rtgl-text c=mu-fg ta=c: Select image - $if layoutType == 'dialogue': - rtgl-view d=h av=c g=sm w=f ph=md pb=md: - rtgl-text s=sm c=mu-fg: Revealing Speed - rtgl-input#revealingSpeedInput type=number min=1 step=1 w=120 value=${previewRevealingSpeed}: null - rtgl-button#playPreviewButton v=ol: Play - $if layoutType == 'choice': - - rtgl-form#choiceForm key=${choiceFormKey} w=f :form=${choiceForm} :defaultValues=${choiceDefaultValues} :context=${choicesContext}: null + - rtgl-form#choiceForm key=${choiceFormKey} w=f :form=${choiceForm} :defaultValues=${choiceDefaultValues} :context=${choicesContext}: + - rtgl-view slot=${previewBackgroundSlot} g=xs: + - rtgl-view#previewBackgroundField w=220 h=160 bw=xs bc=bo br=md cur=pointer av=c ah=c: + - $if previewBackgroundImageId: + - rvn-file-image imageId=${previewBackgroundImageId} source="thumbnail" w=220 h=160 br=md: null + $else: + - rtgl-text c=mu-fg ta=c: Select image - $if layoutType == 'history': - - rtgl-form#historyForm key=${historyFormKey} w=f :form=${historyForm} :defaultValues=${historyDefaultValues} :context=${historyContext}: null + - rtgl-form#historyForm key=${historyFormKey} w=f :form=${historyForm} :defaultValues=${historyDefaultValues} :context=${historyContext}: + - rtgl-view slot=${previewBackgroundSlot} g=xs: + - rtgl-view#previewBackgroundField w=220 h=160 bw=xs bc=bo br=md cur=pointer av=c ah=c: + - $if previewBackgroundImageId: + - rvn-file-image imageId=${previewBackgroundImageId} source="thumbnail" w=220 h=160 br=md: null + $else: + - rtgl-text c=mu-fg ta=c: Select image - $if hasPreviewVariables: - - rtgl-form#previewVariablesForm key=${previewVariablesFormKey} w=f :form=${previewVariablesForm} :defaultValues=${previewVariablesDefaultValues}: null + - rtgl-form#previewVariablesForm key=${previewVariablesFormKey} w=f :form=${previewVariablesForm} :defaultValues=${previewVariablesDefaultValues}: + - rtgl-view slot=${previewBackgroundSlot} g=xs: + - rtgl-view#previewBackgroundField w=220 h=160 bw=xs bc=bo br=md cur=pointer av=c ah=c: + - $if previewBackgroundImageId: + - rvn-file-image imageId=${previewBackgroundImageId} source="thumbnail" w=220 h=160 br=md: null + $else: + - rtgl-text c=mu-fg ta=c: Select image - $if hasSaveLoadPreview: - - rtgl-form#saveLoadForm key=${saveLoadFormKey} w=f :form=${saveLoadForm} :defaultValues=${saveLoadDefaultValues} :context=${saveLoadContext}: null + - rtgl-form#saveLoadForm key=${saveLoadFormKey} w=f :form=${saveLoadForm} :defaultValues=${saveLoadDefaultValues} :context=${saveLoadContext}: + - rtgl-view slot=${previewBackgroundSlot} g=xs: + - rtgl-view#previewBackgroundField w=220 h=160 bw=xs bc=bo br=md cur=pointer av=c ah=c: + - $if previewBackgroundImageId: + - rvn-file-image imageId=${previewBackgroundImageId} source="thumbnail" w=220 h=160 br=md: null + $else: + - rtgl-text c=mu-fg ta=c: Select image + - rtgl-dropdown-menu#dropdownMenu :items=${dropdownMenu.items} x=${dropdownMenu.x} y=${dropdownMenu.y} ?open=${dropdownMenu.isOpen}: null + - rtgl-dialog#imageSelectorDialog ?open=${imageSelectorDialog.open} s=xl: + - rtgl-view slot=content d=v w=f g=md: + - 'rtgl-view d=h w=f h=70vh g=lg style="min-width: 0; min-height: 0;"': + - 'rtgl-view w=300 h=f style="min-width: 0; min-height: 0;"': + - rvn-base-file-explorer#baseFileExplorer :items=${fileExplorerItems} shrinkable: null + - 'rtgl-view w=1fg h=f style="min-width: 0; min-height: 0;"': + - rvn-image-selector#imageSelector selectedImageId=${imageSelectorDialog.selectedImageId}: null + - rtgl-view d=h g=sm w=f ah=e: + - rtgl-button#cancelImageSelection variant=se: Cancel + - rtgl-button#confirmImageSelection variant=pr: OK + - $when: fullImagePreviewVisible + rtgl-view#previewOverlay pos=fix edge=f z=3000 cur=pointer: + - rtgl-view w=f h=f pos=fix edge=f bgc=bg: null + - $if fullImagePreviewImageId: + - rtgl-view pos=fix edge=f ah=c av=c z=3001: + - rvn-file-image imageId=${fullImagePreviewImageId} w=80% h=80% bc=ac bw=xs br=md: null diff --git a/src/components/layoutEditorPreview/support/layoutEditorPreviewData.js b/src/components/layoutEditorPreview/support/layoutEditorPreviewData.js index e2488833..221fd455 100644 --- a/src/components/layoutEditorPreview/support/layoutEditorPreviewData.js +++ b/src/components/layoutEditorPreview/support/layoutEditorPreviewData.js @@ -20,6 +20,7 @@ export const createLayoutEditorPreviewData = ({ previewRevealingSpeed, choicesData, saveLoadData, + backgroundImageId, } = {}) => { const { dialogue, dialogueRevealingSpeed } = createDialoguePreviewData({ layoutType, @@ -29,6 +30,10 @@ export const createLayoutEditorPreviewData = ({ }); return { + backgroundImageId: + typeof backgroundImageId === "string" && backgroundImageId.length > 0 + ? backgroundImageId + : undefined, variables: { ...applyPreviewVariableOverrides( createPreviewVariables(variablesData), diff --git a/src/internal/project/projection.js b/src/internal/project/projection.js index 336a8b82..5a5c07a5 100644 --- a/src/internal/project/projection.js +++ b/src/internal/project/projection.js @@ -1155,7 +1155,7 @@ const LAYOUT_RESOURCE_KEYS = [ "fontFileId", ]; -const TEXT_STYLE_RESOURCE_KEYS = ["colorId", "fontId"]; +const TEXT_STYLE_RESOURCE_KEYS = ["colorId", "strokeColorId", "fontId"]; const EXPORT_RESOURCE_KEYS = new Set([ ...SCENE_RESOURCE_KEYS, @@ -1201,6 +1201,7 @@ const RESOURCE_KEY_TO_TYPES = { hoverTextStyleId: ["textStyles"], clickTextStyleId: ["textStyles"], colorId: ["colors"], + strokeColorId: ["colors"], fontId: ["fonts"], fontFileId: ["fonts"], }; diff --git a/src/pages/layoutEditor/layoutEditor.view.yaml b/src/pages/layoutEditor/layoutEditor.view.yaml index 7840ddcf..2d98a9ac 100644 --- a/src/pages/layoutEditor/layoutEditor.view.yaml +++ b/src/pages/layoutEditor/layoutEditor.view.yaml @@ -51,9 +51,10 @@ template: - rtgl-view w=1fg: - rtgl-text: ${layout.name} - rtgl-button#saveButton sq pre="download" v="gh" ml=sm: null - - rtgl-view h=1fg sv w=f: + - 'rtgl-view d=v h=1fg w=f style="min-height: 0;"': - rvn-layout-editor-canvas#layoutEditorCanvas :resolution=${projectResolution} :layoutState=${layoutState} selectedItemId=${selectedItemId} :previewData=${previewData} :disableMoveDrag=${isInsideDirectedContainer}: null - - rvn-layout-editor-preview#layoutEditorPreview :layoutState=${layoutState}: null + - 'rtgl-view h=1fg w=f sv style="min-height: 0;"': + - rvn-layout-editor-preview#layoutEditorPreview :layoutState=${layoutState}: null - rvn-resizable-panel#resizableDetailPanel panel-type=detail-panel w=300 min-w=200 max-w=500 resize-side="left": - rtgl-view wh=f slot="content": - $if selectedItemId: diff --git a/static/templates/default/repository.json b/static/templates/default/repository.json index 39bc6475..42b4c918 100644 --- a/static/templates/default/repository.json +++ b/static/templates/default/repository.json @@ -775,6 +775,12 @@ "type": "color", "name": "Yellow", "hex": "#FFD84D" + }, + "dL9Qd9q4mW9vM8kWv5wQe": { + "id": "dL9Qd9q4mW9vM8kWv5wQe", + "type": "color", + "name": "Active", + "hex": "#ff5252" } }, "tree": [ @@ -789,6 +795,9 @@ }, { "id": "WT1WD8x7KekvJaUrrf2p9" + }, + { + "id": "dL9Qd9q4mW9vM8kWv5wQe" } ] } @@ -807,6 +816,8 @@ "name": "Default", "fontId": "WbhTa7gjrKgfgb7nYptFb", "colorId": "yqk8aZABQccrrEZUSoAp2", + "strokeColorId": "NqhJ2JeHjnPx8vg9zAJMb", + "strokeWidth": 4, "fontSize": 24, "lineHeight": 1.2, "fontWeight": "400", @@ -818,6 +829,8 @@ "name": "Character Name", "fontId": "WbhTa7gjrKgfgb7nYptFb", "colorId": "yqk8aZABQccrrEZUSoAp2", + "strokeColorId": "NqhJ2JeHjnPx8vg9zAJMb", + "strokeWidth": 4, "fontSize": 48, "lineHeight": 1.5, "fontWeight": "400", @@ -829,6 +842,8 @@ "name": "Text View", "fontId": "WbhTa7gjrKgfgb7nYptFb", "colorId": "yqk8aZABQccrrEZUSoAp2", + "strokeColorId": "NqhJ2JeHjnPx8vg9zAJMb", + "strokeWidth": 4, "fontSize": 36, "lineHeight": 1.5, "fontWeight": "400", @@ -840,6 +855,8 @@ "name": "Default Hover Yellow", "fontId": "WbhTa7gjrKgfgb7nYptFb", "colorId": "WT1WD8x7KekvJaUrrf2p9", + "strokeColorId": "NqhJ2JeHjnPx8vg9zAJMb", + "strokeWidth": 4, "fontSize": 24, "lineHeight": 1.2, "fontWeight": "400", @@ -852,10 +869,11 @@ "fontSize": 36, "lineHeight": 1.5, "colorId": "yqk8aZABQccrrEZUSoAp2", + "strokeColorId": "NqhJ2JeHjnPx8vg9zAJMb", "fontId": "WbhTa7gjrKgfgb7nYptFb", "fontWeight": "700", "previewText": "Menu Text", - "strokeWidth": 0 + "strokeWidth": 4 }, "6wXcAmc_SmJHzMIeKp--Z": { "id": "6wXcAmc_SmJHzMIeKp--Z", @@ -864,10 +882,24 @@ "fontSize": 36, "lineHeight": 1.5, "colorId": "WT1WD8x7KekvJaUrrf2p9", + "strokeColorId": "NqhJ2JeHjnPx8vg9zAJMb", + "fontId": "WbhTa7gjrKgfgb7nYptFb", + "fontWeight": "400", + "previewText": "", + "strokeWidth": 4 + }, + "a8u1N2rL7mV5pQ4tY6wZe": { + "id": "a8u1N2rL7mV5pQ4tY6wZe", + "type": "textStyle", + "name": "Menu Text Active", + "fontSize": 36, + "lineHeight": 1.5, + "colorId": "dL9Qd9q4mW9vM8kWv5wQe", + "strokeColorId": "NqhJ2JeHjnPx8vg9zAJMb", "fontId": "WbhTa7gjrKgfgb7nYptFb", "fontWeight": "400", "previewText": "", - "strokeWidth": 0 + "strokeWidth": 4 } }, "tree": [ @@ -893,6 +925,10 @@ { "id": "6wXcAmc_SmJHzMIeKp--Z", "children": [] + }, + { + "id": "a8u1N2rL7mV5pQ4tY6wZe", + "children": [] } ] } @@ -1506,7 +1542,16 @@ "anchorY": 0, "scaleX": 1, "scaleY": 1, - "rotation": 0 + "rotation": 0, + "hover": { + "inheritToChildren": true + }, + "click": { + "inheritToChildren": true + }, + "rightClick": { + "inheritToChildren": true + } }, "ghg9bBCPKm8MhCAwzH2Vw": { "id": "ghg9bBCPKm8MhCAwzH2Vw", diff --git a/tests/commandLineRuntimeAction/commandLineRuntimeAction.schema.test.js b/tests/commandLineRuntimeAction/commandLineRuntimeAction.schema.test.js new file mode 100644 index 00000000..0681ae8b --- /dev/null +++ b/tests/commandLineRuntimeAction/commandLineRuntimeAction.schema.test.js @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; +import yaml from "js-yaml"; +import { describe, expect, it } from "vitest"; + +describe("commandLineRuntimeAction schema", () => { + it("declares mode and action under an object props schema", () => { + const schemaUrl = new URL( + "../../src/components/commandLineRuntimeAction/commandLineRuntimeAction.schema.yaml", + import.meta.url, + ); + const schemaSource = readFileSync(schemaUrl, "utf8"); + const schema = yaml.load(schemaSource); + + expect(schema).toMatchObject({ + componentName: "rvn-command-line-runtime-action", + propsSchema: { + type: "object", + properties: { + mode: { + type: "string", + }, + action: { + type: "object", + }, + }, + }, + }); + }); +}); diff --git a/tests/project/projection.test.js b/tests/project/projection.test.js index 0af1012c..879d9ff9 100644 --- a/tests/project/projection.test.js +++ b/tests/project/projection.test.js @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { resolveLayoutReferences } from "route-engine-js"; import { buildFilteredStateForExport, collectUsedResourcesForExport, @@ -197,4 +198,147 @@ describe("constructProjectData", () => { }), ]); }); + + it("keeps text style outline color references in filtered export state", () => { + const repositoryState = { + project: { + resolution: { + width: 1920, + height: 1080, + }, + }, + story: { + initialSceneId: "scene-1", + }, + colors: { + items: { + fill: { + id: "fill", + type: "color", + hex: "#ffffff", + }, + stroke: { + id: "stroke", + type: "color", + hex: "#112233", + }, + }, + tree: [{ id: "fill" }, { id: "stroke" }], + }, + fonts: { + items: { + "font-1": { + id: "font-1", + type: "font", + fileId: "font-file-1", + }, + }, + tree: [{ id: "font-1" }], + }, + textStyles: { + items: { + "style-1": { + id: "style-1", + type: "textStyle", + name: "Outlined Text", + fontId: "font-1", + colorId: "fill", + fontSize: 32, + lineHeight: 1.2, + strokeColorId: "stroke", + strokeWidth: 4, + }, + }, + tree: [{ id: "style-1" }], + }, + layouts: { + items: { + "layout-main": { + id: "layout-main", + type: "layout", + name: "Main Layout", + layoutType: "normal", + elements: { + items: { + "text-1": { + id: "text-1", + type: "text", + name: "Preview Text", + x: 10, + y: 20, + width: 300, + height: 80, + text: "Hello", + textStyleId: "style-1", + }, + }, + tree: [{ id: "text-1" }], + }, + }, + }, + tree: [{ id: "layout-main" }], + }, + controls: { + items: {}, + tree: [], + }, + scenes: { + items: { + "scene-1": { + id: "scene-1", + type: "scene", + name: "Scene 1", + sections: { + items: { + "section-1": { + id: "section-1", + name: "Section 1", + lines: { + items: { + "line-1": { + id: "line-1", + actions: { + dialogue: { + ui: { + resourceId: "layout-main", + }, + }, + }, + }, + }, + tree: [{ id: "line-1" }], + }, + }, + }, + tree: [{ id: "section-1" }], + }, + }, + }, + tree: [{ id: "scene-1" }], + }, + }; + + const usage = collectUsedResourcesForExport(repositoryState); + + expect(usage.usedIds.colors).toEqual(expect.arrayContaining(["fill", "stroke"])); + + const filteredState = buildFilteredStateForExport(repositoryState, usage); + const projectData = constructProjectData(filteredState); + const resolvedElements = resolveLayoutReferences( + projectData.resources.layouts["layout-main"].elements, + { + resources: projectData.resources, + }, + ); + + expect(projectData.resources.colors.stroke).toEqual({ + hex: "#112233", + }); + expect(resolvedElements[0].textStyle).toEqual( + expect.objectContaining({ + strokeColor: "#112233", + strokeWidth: 4, + }), + ); + }); });