fix: display module dialog overlays across the full viewport
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
"version": "1.1.21",
|
"version": "1.1.22",
|
||||||
"description": "Autonomy Studio LMS Electron app",
|
"description": "Autonomy Studio LMS Electron app",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
+205
-19
@@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, Menu, globalShortcut, ipcMain, screen } from 'electron'
|
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen } from 'electron'
|
||||||
import type { WebContents } from 'electron'
|
import type { WebContents } from 'electron'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import * as pty from 'node-pty'
|
import * as pty from 'node-pty'
|
||||||
@@ -204,6 +204,17 @@ type CustomModuleScaffoldInput = {
|
|||||||
launchFile: string
|
launchFile: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomModuleImportSource = {
|
||||||
|
sourcePath: string
|
||||||
|
packageName: string
|
||||||
|
launchFiles: string[]
|
||||||
|
buildType: 'ament_python' | 'ament_cmake'
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomModuleImportInput = CustomModuleScaffoldInput & {
|
||||||
|
sourcePath: string
|
||||||
|
}
|
||||||
|
|
||||||
type ModulePrebuiltArtifact = {
|
type ModulePrebuiltArtifact = {
|
||||||
type?: string | null
|
type?: string | null
|
||||||
url?: string | null
|
url?: string | null
|
||||||
@@ -931,19 +942,152 @@ function validateCustomModuleScaffold(input: CustomModuleScaffoldInput): CustomM
|
|||||||
return { name, category, folderName, packageName, launchFile, dockerImage }
|
return { name, category, folderName, packageName, launchFile, dockerImage }
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record<string, string> {
|
function customModuleConfigContent(input: CustomModuleScaffoldInput): string {
|
||||||
const module = validateCustomModuleScaffold(input)
|
const module = validateCustomModuleScaffold(input)
|
||||||
const executable = `${module.packageName}_node`
|
return `${JSON.stringify({
|
||||||
const nodePath = `${module.packageName}/node.py`
|
|
||||||
return {
|
|
||||||
'.autonomy-studio-module.json': `${JSON.stringify({
|
|
||||||
sourceType: 'CUSTOM',
|
sourceType: 'CUSTOM',
|
||||||
name: module.name,
|
name: module.name,
|
||||||
category: module.category,
|
category: module.category,
|
||||||
dockerImage: module.dockerImage,
|
dockerImage: module.dockerImage,
|
||||||
packageName: module.packageName,
|
packageName: module.packageName,
|
||||||
launchFile: module.launchFile,
|
launchFile: module.launchFile,
|
||||||
}, null, 2)}\n`,
|
}, null, 2)}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCustomModuleImportSource(sourcePath: string): Promise<CustomModuleImportSource> {
|
||||||
|
const selectedPath = sourcePath.trim()
|
||||||
|
if (!selectedPath) throw new Error('불러올 모듈 폴더를 선택하세요.')
|
||||||
|
|
||||||
|
const selectedRoot = resolve(selectedPath)
|
||||||
|
let packageRoot = selectedRoot
|
||||||
|
try {
|
||||||
|
if (!(await stat(selectedRoot)).isDirectory()) throw new Error('not-a-directory')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === 'not-a-directory') {
|
||||||
|
throw new Error('선택한 경로가 폴더가 아닙니다.')
|
||||||
|
}
|
||||||
|
throw new Error('선택한 모듈 폴더를 찾을 수 없습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(join(packageRoot, 'package.xml'))) {
|
||||||
|
const sourceDirectory = join(selectedRoot, 'src')
|
||||||
|
let packageDirectories: string[] = []
|
||||||
|
try {
|
||||||
|
packageDirectories = (await readdir(sourceDirectory, { withFileTypes: true }))
|
||||||
|
.filter((entry) => entry.isDirectory() && existsSync(join(sourceDirectory, entry.name, 'package.xml')))
|
||||||
|
.map((entry) => join(sourceDirectory, entry.name))
|
||||||
|
} catch {
|
||||||
|
// The error below explains the required ROS workspace structure.
|
||||||
|
}
|
||||||
|
if (packageDirectories.length === 0) {
|
||||||
|
throw new Error('package.xml을 찾을 수 없습니다. ROS2 패키지 폴더 또는 src 아래에 패키지가 하나 있는 워크스페이스를 선택하세요.')
|
||||||
|
}
|
||||||
|
if (packageDirectories.length > 1) {
|
||||||
|
throw new Error('여러 ROS2 패키지가 있습니다. 불러올 패키지 폴더를 직접 선택하세요.')
|
||||||
|
}
|
||||||
|
packageRoot = packageDirectories[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageXml = await readFile(join(packageRoot, 'package.xml'), 'utf8')
|
||||||
|
const packageName = packageXml.match(/<name>\s*([^<]+?)\s*<\/name>/)?.[1]?.trim() ?? ''
|
||||||
|
if (!/^[a-z][a-z0-9_]*$/.test(packageName)) {
|
||||||
|
throw new Error('package.xml의 패키지명이 ROS2 패키지명 형식과 맞지 않습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildType = /<buildtool_depend>\s*ament_python\s*<\/buildtool_depend>/.test(packageXml)
|
||||||
|
? 'ament_python'
|
||||||
|
: /<buildtool_depend>\s*ament_cmake\s*<\/buildtool_depend>/.test(packageXml)
|
||||||
|
? 'ament_cmake'
|
||||||
|
: null
|
||||||
|
if (!buildType) throw new Error('ament_python 또는 ament_cmake 기반 ROS2 패키지만 불러올 수 있습니다.')
|
||||||
|
if (buildType === 'ament_python' && !existsSync(join(packageRoot, 'setup.py'))) {
|
||||||
|
throw new Error('ament_python 패키지에 setup.py가 없습니다.')
|
||||||
|
}
|
||||||
|
if (buildType === 'ament_cmake' && !existsSync(join(packageRoot, 'CMakeLists.txt'))) {
|
||||||
|
throw new Error('ament_cmake 패키지에 CMakeLists.txt가 없습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
let launchFiles: string[] = []
|
||||||
|
try {
|
||||||
|
launchFiles = (await readdir(join(packageRoot, 'launch'), { withFileTypes: true }))
|
||||||
|
.filter((entry) => entry.isFile() && /^[a-zA-Z0-9_.-]+\.launch\.py$/.test(entry.name))
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.sort((left, right) => left.localeCompare(right))
|
||||||
|
} catch {
|
||||||
|
// The validation message below covers a missing launch directory too.
|
||||||
|
}
|
||||||
|
if (launchFiles.length === 0) {
|
||||||
|
throw new Error('launch 폴더에 실행 가능한 *.launch.py 파일이 없습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sourcePath: packageRoot, packageName, launchFiles, buildType }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectCustomModuleImportSource(): Promise<CustomModuleImportSource | null> {
|
||||||
|
const selection = await dialog.showOpenDialog({
|
||||||
|
title: '모듈 불러오기',
|
||||||
|
buttonLabel: '선택',
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
})
|
||||||
|
if (selection.canceled || !selection.filePaths[0]) return null
|
||||||
|
return resolveCustomModuleImportSource(selection.filePaths[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignoredCustomModuleImportDirectories = new Set([
|
||||||
|
'.git',
|
||||||
|
'.mypy_cache',
|
||||||
|
'.pytest_cache',
|
||||||
|
'__pycache__',
|
||||||
|
'build',
|
||||||
|
'install',
|
||||||
|
'log',
|
||||||
|
'node_modules',
|
||||||
|
])
|
||||||
|
|
||||||
|
async function importCustomWorkspaceModule(
|
||||||
|
workspaceRoot: string | null | undefined,
|
||||||
|
input: CustomModuleImportInput,
|
||||||
|
): Promise<{ ok: boolean; workspacePath: string }> {
|
||||||
|
const module = validateCustomModuleScaffold(input)
|
||||||
|
const source = await resolveCustomModuleImportSource(input.sourcePath)
|
||||||
|
if (source.packageName !== module.packageName || !source.launchFiles.includes(module.launchFile)) {
|
||||||
|
throw new Error('선택한 소스의 패키지 또는 Launch 파일 검증 결과가 현재 입력값과 다릅니다. 다시 선택하세요.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedWorkspaceRoot = expandUserPath(workspaceRoot)
|
||||||
|
if (!resolvedWorkspaceRoot) throw new Error('워크스페이스 경로가 설정되어 있지 않습니다.')
|
||||||
|
const moduleRoot = resolve(resolvedWorkspaceRoot, module.folderName)
|
||||||
|
if (moduleRoot === resolvedWorkspaceRoot || !moduleRoot.startsWith(`${resolvedWorkspaceRoot}/`)) {
|
||||||
|
throw new Error('허용되지 않은 모듈 경로입니다.')
|
||||||
|
}
|
||||||
|
if (existsSync(moduleRoot)) throw new Error('같은 이름의 모듈이 이미 현재 워크스페이스에 있습니다.')
|
||||||
|
|
||||||
|
await mkdir(resolvedWorkspaceRoot, { recursive: true })
|
||||||
|
try {
|
||||||
|
await cp(source.sourcePath, moduleRoot, {
|
||||||
|
recursive: true,
|
||||||
|
errorOnExist: true,
|
||||||
|
force: false,
|
||||||
|
filter: (currentPath) => {
|
||||||
|
const pathParts = relative(source.sourcePath, currentPath).split(/[\\/]/).filter(Boolean)
|
||||||
|
return !pathParts.some((part) => ignoredCustomModuleImportDirectories.has(part))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await writeFile(resolve(moduleRoot, '.autonomy-studio-module.json'), customModuleConfigContent(module), { encoding: 'utf8', flag: 'wx' })
|
||||||
|
await runCommand('git', ['init', '--initial-branch=main'], moduleRoot, undefined, 15_000).catch(() => undefined)
|
||||||
|
} catch (error) {
|
||||||
|
await rm(moduleRoot, { recursive: true, force: true }).catch(() => undefined)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return { ok: true, workspacePath: moduleRoot }
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record<string, string> {
|
||||||
|
const module = validateCustomModuleScaffold(input)
|
||||||
|
const executable = `${module.packageName}_node`
|
||||||
|
const nodePath = `${module.packageName}/node.py`
|
||||||
|
return {
|
||||||
|
'.autonomy-studio-module.json': customModuleConfigContent(module),
|
||||||
'.gitignore': 'build/\ninstall/\nlog/\n__pycache__/\n*.py[cod]\n',
|
'.gitignore': 'build/\ninstall/\nlog/\n__pycache__/\n*.py[cod]\n',
|
||||||
'package.xml': [
|
'package.xml': [
|
||||||
'<?xml version="1.0"?>',
|
'<?xml version="1.0"?>',
|
||||||
@@ -4058,6 +4202,7 @@ function buildFastDdsPeerConfig(peerAddress: string, localAddress: string, rosDo
|
|||||||
discoveryBasePort,
|
discoveryBasePort,
|
||||||
...Array.from({ length: 121 }, (_, participantId) => discoveryBasePort + 10 + (2 * participantId))
|
...Array.from({ length: 121 }, (_, participantId) => discoveryBasePort + 10 + (2 * participantId))
|
||||||
]
|
]
|
||||||
|
const localAddresses = Array.from(new Set(['127.0.0.1', localAddress]))
|
||||||
const peerAddresses = Array.from(new Set(['127.0.0.1', peerAddress]))
|
const peerAddresses = Array.from(new Set(['127.0.0.1', peerAddress]))
|
||||||
return [
|
return [
|
||||||
'<?xml version="1.0" encoding="UTF-8" ?>',
|
'<?xml version="1.0" encoding="UTF-8" ?>',
|
||||||
@@ -4070,8 +4215,7 @@ function buildFastDdsPeerConfig(peerAddress: string, localAddress: string, rosDo
|
|||||||
' <receiveBufferSize>1048576</receiveBufferSize>',
|
' <receiveBufferSize>1048576</receiveBufferSize>',
|
||||||
' <maxMessageSize>1200</maxMessageSize>',
|
' <maxMessageSize>1200</maxMessageSize>',
|
||||||
' <interfaceWhiteList>',
|
' <interfaceWhiteList>',
|
||||||
' <address>127.0.0.1</address>',
|
...localAddresses.map((address) => ` <address>${address}</address>`),
|
||||||
` <address>${localAddress}</address>`,
|
|
||||||
' </interfaceWhiteList>',
|
' </interfaceWhiteList>',
|
||||||
' </transport_descriptor>',
|
' </transport_descriptor>',
|
||||||
' </transport_descriptors>',
|
' </transport_descriptors>',
|
||||||
@@ -4083,12 +4227,14 @@ function buildFastDdsPeerConfig(peerAddress: string, localAddress: string, rosDo
|
|||||||
' </userTransports>',
|
' </userTransports>',
|
||||||
' <builtin>',
|
' <builtin>',
|
||||||
' <metatrafficUnicastLocatorList>',
|
' <metatrafficUnicastLocatorList>',
|
||||||
|
...localAddresses.flatMap((address) => [
|
||||||
' <locator>',
|
' <locator>',
|
||||||
' <udpv4>',
|
' <udpv4>',
|
||||||
` <address>${localAddress}</address>`,
|
` <address>${address}</address>`,
|
||||||
' <port>0</port>',
|
' <port>0</port>',
|
||||||
' </udpv4>',
|
' </udpv4>',
|
||||||
' </locator>',
|
' </locator>',
|
||||||
|
]),
|
||||||
' </metatrafficUnicastLocatorList>',
|
' </metatrafficUnicastLocatorList>',
|
||||||
' <initialPeersList>',
|
' <initialPeersList>',
|
||||||
...peerAddresses.flatMap((address) => peers.flatMap((port) => [
|
...peerAddresses.flatMap((address) => peers.flatMap((port) => [
|
||||||
@@ -4269,9 +4415,29 @@ function buildNav2DockerRunScript(): string {
|
|||||||
'FASTDDS_CONFIG_PATH=${FASTDDS_CONFIG_PATH:-}',
|
'FASTDDS_CONFIG_PATH=${FASTDDS_CONFIG_PATH:-}',
|
||||||
'ROS_LOCALHOST_ONLY_VALUE=${ROS_LOCALHOST_ONLY:-1}',
|
'ROS_LOCALHOST_ONLY_VALUE=${ROS_LOCALHOST_ONLY:-1}',
|
||||||
'LOCALIZATION_MODE=slam',
|
'LOCALIZATION_MODE=slam',
|
||||||
|
'WORLD_LAYOUT_PATH_VALUE=/modules/world-layout.json',
|
||||||
'filtered_args=()',
|
'filtered_args=()',
|
||||||
'for arg in "$@"; do',
|
'for arg in "$@"; do',
|
||||||
' if [ "$arg" = "localization_mode:=amcl" ]; then LOCALIZATION_MODE=amcl; else filtered_args+=("$arg"); fi',
|
' case "$arg" in',
|
||||||
|
' localization_mode:=amcl)',
|
||||||
|
' LOCALIZATION_MODE=amcl',
|
||||||
|
' ;;',
|
||||||
|
' world_layout_path:=*)',
|
||||||
|
' candidate_path=${arg#world_layout_path:=}',
|
||||||
|
' case "$candidate_path" in',
|
||||||
|
' /modules/maps/*.gazebo-layout.json|/modules/maps/*.gazebo-3d-layout.json)',
|
||||||
|
' WORLD_LAYOUT_PATH_VALUE=$candidate_path',
|
||||||
|
' ;;',
|
||||||
|
' *)',
|
||||||
|
' echo "Invalid saved map world layout path: $candidate_path" >&2',
|
||||||
|
' exit 64',
|
||||||
|
' ;;',
|
||||||
|
' esac',
|
||||||
|
' ;;',
|
||||||
|
' *)',
|
||||||
|
' filtered_args+=("$arg")',
|
||||||
|
' ;;',
|
||||||
|
' esac',
|
||||||
'done',
|
'done',
|
||||||
'set -- "${filtered_args[@]}"',
|
'set -- "${filtered_args[@]}"',
|
||||||
'',
|
'',
|
||||||
@@ -4332,6 +4498,7 @@ function buildNav2DockerRunScript(): string {
|
|||||||
' -e "TURTLEBOT3_MODEL=${TURTLEBOT3_MODEL:-burger}"',
|
' -e "TURTLEBOT3_MODEL=${TURTLEBOT3_MODEL:-burger}"',
|
||||||
' -e "MODULE_ROOT=/modules"',
|
' -e "MODULE_ROOT=/modules"',
|
||||||
' -e "MODULE_FOLDER=$MODULE_FOLDER"',
|
' -e "MODULE_FOLDER=$MODULE_FOLDER"',
|
||||||
|
' -e "WORLD_LAYOUT_PATH=$WORLD_LAYOUT_PATH_VALUE"',
|
||||||
' -e "GAZEBO_MODEL_PATH=/usr/share/gazebo-11/models:/modules/[lms-nav2-simulator]nav2-simulator_v1.0.0_ws/install/turtlebot3_gazebo/share/turtlebot3_gazebo/models:/modules/[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws/install/turtlebot3_gazebo/share/turtlebot3_gazebo/models"',
|
' -e "GAZEBO_MODEL_PATH=/usr/share/gazebo-11/models:/modules/[lms-nav2-simulator]nav2-simulator_v1.0.0_ws/install/turtlebot3_gazebo/share/turtlebot3_gazebo/models:/modules/[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws/install/turtlebot3_gazebo/share/turtlebot3_gazebo/models"',
|
||||||
' -v "$MODULE_ROOT:/modules"',
|
' -v "$MODULE_ROOT:/modules"',
|
||||||
' -v "$(dirname "$0"):/runtime/nav2/docker:ro"',
|
' -v "$(dirname "$0"):/runtime/nav2/docker:ro"',
|
||||||
@@ -5514,8 +5681,24 @@ function quoteShellArg(value: string): string {
|
|||||||
return `'${value.replace(/'/g, `'\\''`)}'`
|
return `'${value.replace(/'/g, `'\\''`)}'`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWorkspaceShellCommand(moduleName: string | undefined, terminalCwd: string, cols: number, rows: number): string {
|
function buildWorkspaceShellCommand(
|
||||||
|
moduleName: string | undefined,
|
||||||
|
terminalCwd: string,
|
||||||
|
cols: number,
|
||||||
|
rows: number,
|
||||||
|
restrictToModuleWorkspace: boolean,
|
||||||
|
): string {
|
||||||
const promptLabel = (moduleName?.trim() || basename(terminalCwd)).replace(/[^a-zA-Z0-9_.-]+/g, '-')
|
const promptLabel = (moduleName?.trim() || basename(terminalCwd)).replace(/[^a-zA-Z0-9_.-]+/g, '-')
|
||||||
|
const navigationCommands = restrictToModuleWorkspace
|
||||||
|
? [
|
||||||
|
'__module_workspace_guard() { if [[ "$PWD" == "$MODULE_WORKSPACE_ROOT" || "$PWD" == "$MODULE_WORKSPACE_ROOT"/* ]]; then return 0; fi; builtin cd "$MODULE_WORKSPACE_ROOT"; printf "\\n모듈 워크스페이스 밖으로 이동할 수 없습니다.\\n"; return 1; }',
|
||||||
|
'cd() { builtin cd "$@" || return; __module_workspace_guard; }',
|
||||||
|
'export PROMPT_COMMAND=\'__module_workspace_guard; if [[ "$PWD" == "$MODULE_WORKSPACE_ROOT" ]]; then __rel=""; else __rel="${PWD:$(( ${#MODULE_WORKSPACE_ROOT} + 1 ))}"; fi; PS1="\\[\\e[1;34m\\]${MODULE_PROMPT_LABEL}${__rel:+/$__rel}\\[\\e[0m\\]: "\'',
|
||||||
|
'export -f __module_workspace_guard cd',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'export PROMPT_COMMAND=\'if [[ "$PWD" == "$MODULE_WORKSPACE_ROOT" ]]; then __rel=""; elif [[ "$PWD" == "$MODULE_WORKSPACE_ROOT"/* ]]; then __rel="${PWD:$(( ${#MODULE_WORKSPACE_ROOT} + 1 ))}"; else __rel=".../${PWD##*/}"; fi; PS1="\\[\\e[1;34m\\]${MODULE_PROMPT_LABEL}${__rel:+/$__rel}\\[\\e[0m\\]: "\'',
|
||||||
|
]
|
||||||
|
|
||||||
const initCommands = [
|
const initCommands = [
|
||||||
`stty cols ${cols} rows ${rows}`,
|
`stty cols ${cols} rows ${rows}`,
|
||||||
@@ -5526,10 +5709,8 @@ function buildWorkspaceShellCommand(moduleName: string | undefined, terminalCwd:
|
|||||||
'grep() { command grep --color=auto "$@"; }',
|
'grep() { command grep --color=auto "$@"; }',
|
||||||
'egrep() { command egrep --color=auto "$@"; }',
|
'egrep() { command egrep --color=auto "$@"; }',
|
||||||
'fgrep() { command fgrep --color=auto "$@"; }',
|
'fgrep() { command fgrep --color=auto "$@"; }',
|
||||||
'__module_workspace_guard() { if [[ "$PWD" == "$MODULE_WORKSPACE_ROOT" || "$PWD" == "$MODULE_WORKSPACE_ROOT"/* ]]; then return 0; fi; builtin cd "$MODULE_WORKSPACE_ROOT"; printf "\\n모듈 워크스페이스 밖으로 이동할 수 없습니다.\\n"; return 1; }',
|
'export -f ls grep egrep fgrep',
|
||||||
'cd() { builtin cd "$@" || return; __module_workspace_guard; }',
|
...navigationCommands,
|
||||||
'export PROMPT_COMMAND=\'__module_workspace_guard; if [[ "$PWD" == "$MODULE_WORKSPACE_ROOT" ]]; then __rel=""; else __rel="${PWD:$(( ${#MODULE_WORKSPACE_ROOT} + 1 ))}"; fi; PS1="\\[\\e[1;34m\\]${MODULE_PROMPT_LABEL}${__rel:+/$__rel}\\[\\e[0m\\]: "\'',
|
|
||||||
'export -f ls grep egrep fgrep __module_workspace_guard cd',
|
|
||||||
'exec /bin/bash --noprofile --norc -i'
|
'exec /bin/bash --noprofile --norc -i'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5627,7 +5808,7 @@ function registerTerminalHandlers(): void {
|
|||||||
|
|
||||||
const safeCols = Math.max(20, Math.min(300, Math.floor(cols ?? 80)))
|
const safeCols = Math.max(20, Math.min(300, Math.floor(cols ?? 80)))
|
||||||
const safeRows = Math.max(5, Math.min(120, Math.floor(rows ?? 24)))
|
const safeRows = Math.max(5, Math.min(120, Math.floor(rows ?? 24)))
|
||||||
const proc = pty.spawn('/bin/bash', ['--noprofile', '--norc', '-c', buildWorkspaceShellCommand(moduleName, terminalCwd, safeCols, safeRows)], {
|
const proc = pty.spawn('/bin/bash', ['--noprofile', '--norc', '-c', buildWorkspaceShellCommand(moduleName, terminalCwd, safeCols, safeRows, false)], {
|
||||||
cwd: terminalCwd,
|
cwd: terminalCwd,
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
cols: safeCols,
|
cols: safeCols,
|
||||||
@@ -5657,7 +5838,7 @@ function registerTerminalHandlers(): void {
|
|||||||
|
|
||||||
const safeCols = Math.max(20, Math.min(300, Math.floor(cols ?? 80)))
|
const safeCols = Math.max(20, Math.min(300, Math.floor(cols ?? 80)))
|
||||||
const safeRows = Math.max(5, Math.min(120, Math.floor(rows ?? 24)))
|
const safeRows = Math.max(5, Math.min(120, Math.floor(rows ?? 24)))
|
||||||
const proc = pty.spawn('/bin/bash', ['--noprofile', '--norc', '-c', buildWorkspaceShellCommand(moduleName, terminalCwd, safeCols, safeRows)], {
|
const proc = pty.spawn('/bin/bash', ['--noprofile', '--norc', '-c', buildWorkspaceShellCommand(moduleName, terminalCwd, safeCols, safeRows, false)], {
|
||||||
cwd: terminalCwd,
|
cwd: terminalCwd,
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
cols: safeCols,
|
cols: safeCols,
|
||||||
@@ -5701,7 +5882,7 @@ function registerTerminalHandlers(): void {
|
|||||||
'-i',
|
'-i',
|
||||||
target.identityFile,
|
target.identityFile,
|
||||||
`${target.user}@${target.host}`,
|
`${target.user}@${target.host}`,
|
||||||
buildWorkspaceShellCommand(moduleName, resolved.moduleRoot, safeCols, safeRows)
|
buildWorkspaceShellCommand(moduleName, resolved.moduleRoot, safeCols, safeRows, true)
|
||||||
], {
|
], {
|
||||||
name: 'xterm-256color',
|
name: 'xterm-256color',
|
||||||
cols: safeCols,
|
cols: safeCols,
|
||||||
@@ -5970,6 +6151,11 @@ function registerLocalWorkspaceHandlers(): void {
|
|||||||
if (!input) throw new Error('커스텀 모듈 정보가 없습니다.')
|
if (!input) throw new Error('커스텀 모듈 정보가 없습니다.')
|
||||||
return createCustomWorkspaceModule(workspaceRoot, input)
|
return createCustomWorkspaceModule(workspaceRoot, input)
|
||||||
})
|
})
|
||||||
|
ipcMain.handle('local-workspace:modules:select-import-source', () => selectCustomModuleImportSource())
|
||||||
|
ipcMain.handle('local-workspace:modules:import-custom', (_event, workspaceRoot?: string | null, input?: CustomModuleImportInput | null) => {
|
||||||
|
if (!input) throw new Error('불러올 모듈 정보가 없습니다.')
|
||||||
|
return importCustomWorkspaceModule(workspaceRoot, input)
|
||||||
|
})
|
||||||
ipcMain.handle('local-workspace:modules:delete', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null) => deleteWorkspaceModule(moduleName, workspaceRoot, folderName))
|
ipcMain.handle('local-workspace:modules:delete', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null) => deleteWorkspaceModule(moduleName, workspaceRoot, folderName))
|
||||||
ipcMain.handle('local-workspace:modules:start', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, rosParams?: string | null, gpuEnabled?: boolean | null, rosDomainId?: string | null, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => startWorkspaceModule(moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork))
|
ipcMain.handle('local-workspace:modules:start', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, rosParams?: string | null, gpuEnabled?: boolean | null, rosDomainId?: string | null, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => startWorkspaceModule(moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork))
|
||||||
ipcMain.handle('local-workspace:modules:build', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, gpuEnabled?: boolean | null, rosDomainId?: string | null, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string | null) => buildWorkspaceModule(moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages, rosParams))
|
ipcMain.handle('local-workspace:modules:build', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, gpuEnabled?: boolean | null, rosDomainId?: string | null, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string | null) => buildWorkspaceModule(moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages, rosParams))
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ contextBridge.exposeInMainWorld('localWorkspace', {
|
|||||||
listDockerImages: () => ipcRenderer.invoke('local-workspace:modules:docker-images'),
|
listDockerImages: () => ipcRenderer.invoke('local-workspace:modules:docker-images'),
|
||||||
install: (moduleName: string, workspaceRoot?: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('local-workspace:modules:install', moduleName, workspaceRoot, folderName, templateRepositoryUrl, studentRepositoryUrl, options),
|
install: (moduleName: string, workspaceRoot?: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('local-workspace:modules:install', moduleName, workspaceRoot, folderName, templateRepositoryUrl, studentRepositoryUrl, options),
|
||||||
createCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string }) => ipcRenderer.invoke('local-workspace:modules:create-custom', workspaceRoot, input),
|
createCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string }) => ipcRenderer.invoke('local-workspace:modules:create-custom', workspaceRoot, input),
|
||||||
|
selectImportSource: () => ipcRenderer.invoke('local-workspace:modules:select-import-source'),
|
||||||
|
importCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string; sourcePath: string }) => ipcRenderer.invoke('local-workspace:modules:import-custom', workspaceRoot, input),
|
||||||
delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:delete', moduleName, workspaceRoot, folderName),
|
delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:delete', moduleName, workspaceRoot, folderName),
|
||||||
start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('local-workspace:modules:start', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork),
|
start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('local-workspace:modules:start', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork),
|
||||||
build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string) => ipcRenderer.invoke('local-workspace:modules:build', moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages, rosParams),
|
build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string) => ipcRenderer.invoke('local-workspace:modules:build', moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages, rosParams),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import CurriculumHomeScreen from './features/curriculum/CurriculumHomeScreen'
|
|||||||
import InstructorStudioNoticeModal from './features/curriculum/InstructorStudioNoticeModal'
|
import InstructorStudioNoticeModal from './features/curriculum/InstructorStudioNoticeModal'
|
||||||
import MaterialsModal from './features/curriculum/MaterialsModal'
|
import MaterialsModal from './features/curriculum/MaterialsModal'
|
||||||
import { enrollInCourse, fetchCurriculum, fetchStudentCourses, getDownloadResourceSizeLabel, getDownloadResourceWeek, resolveDownloadUrl, searchCourseCatalog } from './features/curriculum/curriculumApi'
|
import { enrollInCourse, fetchCurriculum, fetchStudentCourses, getDownloadResourceSizeLabel, getDownloadResourceWeek, resolveDownloadUrl, searchCourseCatalog } from './features/curriculum/curriculumApi'
|
||||||
import type { CustomModuleInput } from './features/workspace/ModuleListView'
|
import type { CustomModuleImportInput, CustomModuleInput } from './features/workspace/ModuleListView'
|
||||||
import type { CurriculumWeek, DownloadResource, LearningStat, StudentCourseOption } from './features/curriculum/types'
|
import type { CurriculumWeek, DownloadResource, LearningStat, StudentCourseOption } from './features/curriculum/types'
|
||||||
import { getContextHelpSteps } from './features/help/contextHelp'
|
import { getContextHelpSteps } from './features/help/contextHelp'
|
||||||
import AiGuideView from './features/AiGuideView'
|
import AiGuideView from './features/AiGuideView'
|
||||||
@@ -3292,7 +3292,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
openEditorFile(editorPath)
|
openEditorFile(editorPath)
|
||||||
setActiveIdeActivity('workspace')
|
setActiveIdeActivity('workspace')
|
||||||
}
|
}
|
||||||
const createCustomModule = async (input: CustomModuleInput): Promise<void> => {
|
const registerCustomModule = async (input: CustomModuleInput): Promise<NonNullable<CurriculumWeek['modules']>[number]> => {
|
||||||
const response = await fetchWithFreshAuth(`${LMS_API_BASE_URL}/curriculum/custom-modules`, {
|
const response = await fetchWithFreshAuth(`${LMS_API_BASE_URL}/curriculum/custom-modules`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
@@ -3333,17 +3333,47 @@ function LocalPcWorkspaceMockup({
|
|||||||
launchFile: payload.launchFile ?? input.launchFile,
|
launchFile: payload.launchFile ?? input.launchFile,
|
||||||
runtimeTarget: payload.runtimeTarget ?? customModuleRuntimeTarget,
|
runtimeTarget: payload.runtimeTarget ?? customModuleRuntimeTarget,
|
||||||
} satisfies NonNullable<CurriculumWeek['modules']>[number]
|
} satisfies NonNullable<CurriculumWeek['modules']>[number]
|
||||||
if (remoteWorkspace) {
|
return customModule
|
||||||
await window.remoteWorkspace.modules.createCustom(getRemoteWorkspaceTarget(), customModule)
|
|
||||||
} else {
|
|
||||||
await window.localWorkspace.modules.createCustom(clonePathPreview, customModule)
|
|
||||||
}
|
}
|
||||||
|
const addCustomModuleToList = (customModule: NonNullable<CurriculumWeek['modules']>[number]): void => {
|
||||||
setCustomModules((current) => [
|
setCustomModules((current) => [
|
||||||
...current,
|
...current,
|
||||||
{ ...customModule, sortOrder: payload.sortOrder ?? 10_000 + current.length },
|
{ ...customModule, sortOrder: customModule.sortOrder ?? 10_000 + current.length },
|
||||||
])
|
])
|
||||||
|
}
|
||||||
|
const createCustomModule = async (input: CustomModuleInput): Promise<void> => {
|
||||||
|
const customModule = await registerCustomModule(input)
|
||||||
|
const workspaceModule = {
|
||||||
|
name: customModule.name,
|
||||||
|
category: customModule.category,
|
||||||
|
folderName: customModule.folderName,
|
||||||
|
dockerImage: customModule.dockerImage ?? null,
|
||||||
|
packageName: customModule.packageName ?? input.packageName,
|
||||||
|
launchFile: customModule.launchFile ?? input.launchFile,
|
||||||
|
}
|
||||||
|
if (remoteWorkspace) {
|
||||||
|
await window.remoteWorkspace.modules.createCustom(getRemoteWorkspaceTarget(), workspaceModule)
|
||||||
|
} else {
|
||||||
|
await window.localWorkspace.modules.createCustom(clonePathPreview, workspaceModule)
|
||||||
|
}
|
||||||
|
addCustomModuleToList(customModule)
|
||||||
onTitlebarNotice('커스텀 모듈이 저장되었습니다.', 'info')
|
onTitlebarNotice('커스텀 모듈이 저장되었습니다.', 'info')
|
||||||
}
|
}
|
||||||
|
const importCustomModule = async ({ sourcePath, ...input }: CustomModuleImportInput): Promise<void> => {
|
||||||
|
if (remoteWorkspace) throw new Error('모듈 불러오기는 로컬 워크스페이스에서만 사용할 수 있습니다.')
|
||||||
|
const customModule = await registerCustomModule(input)
|
||||||
|
await window.localWorkspace.modules.importCustom(clonePathPreview, {
|
||||||
|
name: customModule.name,
|
||||||
|
category: customModule.category,
|
||||||
|
folderName: customModule.folderName,
|
||||||
|
dockerImage: customModule.dockerImage ?? null,
|
||||||
|
packageName: customModule.packageName ?? input.packageName,
|
||||||
|
launchFile: customModule.launchFile ?? input.launchFile,
|
||||||
|
sourcePath,
|
||||||
|
})
|
||||||
|
addCustomModuleToList(customModule)
|
||||||
|
onTitlebarNotice('모듈을 현재 워크스페이스로 불러왔습니다.', 'info')
|
||||||
|
}
|
||||||
const controlEndTime = controlEndsAt ? new Date(controlEndsAt).getTime() : Number.NaN
|
const controlEndTime = controlEndsAt ? new Date(controlEndsAt).getTime() : Number.NaN
|
||||||
const controlRemainingMs = Number.isNaN(controlEndTime) ? null : Math.max(0, controlEndTime - controlCountdownNow)
|
const controlRemainingMs = Number.isNaN(controlEndTime) ? null : Math.max(0, controlEndTime - controlCountdownNow)
|
||||||
const controlRemainingSeconds = controlRemainingMs === null ? null : Math.ceil(controlRemainingMs / 1000)
|
const controlRemainingSeconds = controlRemainingMs === null ? null : Math.ceil(controlRemainingMs / 1000)
|
||||||
@@ -3911,6 +3941,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
setWorldMapSelectorOpen(true)
|
setWorldMapSelectorOpen(true)
|
||||||
}}
|
}}
|
||||||
onCreateCustomModule={createCustomModule}
|
onCreateCustomModule={createCustomModule}
|
||||||
|
onImportCustomModule={importCustomModule}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Vendored
+7
@@ -127,6 +127,13 @@ interface Window {
|
|||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
createCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string }) => Promise<{ ok: boolean; workspacePath: string }>
|
createCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string }) => Promise<{ ok: boolean; workspacePath: string }>
|
||||||
|
selectImportSource: () => Promise<{
|
||||||
|
sourcePath: string
|
||||||
|
packageName: string
|
||||||
|
launchFiles: string[]
|
||||||
|
buildType: 'ament_python' | 'ament_cmake'
|
||||||
|
} | null>
|
||||||
|
importCustom: (workspaceRoot: string | undefined, input: { name: string; category: string; folderName: string; dockerImage?: string | null; packageName: string; launchFile: string; sourcePath: string }) => Promise<{ ok: boolean; workspacePath: string }>
|
||||||
delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => Promise<{ ok: boolean }>
|
delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => Promise<{ ok: boolean }>
|
||||||
start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => Promise<{ ok: boolean; message: string }>
|
start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => Promise<{ ok: boolean; message: string }>
|
||||||
build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string) => Promise<{ ok: boolean; message: string }>
|
build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null, rosParams?: string) => Promise<{ ok: boolean; message: string }>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BookOpen, ChevronLeft, ChevronRight, PackagePlus, X } from 'lucide-react'
|
import { BookOpen, ChevronLeft, ChevronRight, FolderOpen, PackagePlus, X } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import ModuleDeleteConfirm from './ModuleDeleteConfirm'
|
import ModuleDeleteConfirm from './ModuleDeleteConfirm'
|
||||||
@@ -46,6 +46,7 @@ type ModuleListViewProps = {
|
|||||||
parkingHeightStorageKey?: string
|
parkingHeightStorageKey?: string
|
||||||
onOpenWorldEditor: () => void
|
onOpenWorldEditor: () => void
|
||||||
onCreateCustomModule: (input: CustomModuleInput) => Promise<void>
|
onCreateCustomModule: (input: CustomModuleInput) => Promise<void>
|
||||||
|
onImportCustomModule: (input: CustomModuleImportInput) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModuleListView({
|
export default function ModuleListView({
|
||||||
@@ -80,12 +81,14 @@ export default function ModuleListView({
|
|||||||
onRestoreParkedModule,
|
onRestoreParkedModule,
|
||||||
parkingHeightStorageKey,
|
parkingHeightStorageKey,
|
||||||
onOpenWorldEditor,
|
onOpenWorldEditor,
|
||||||
onCreateCustomModule
|
onCreateCustomModule,
|
||||||
|
onImportCustomModule
|
||||||
}: ModuleListViewProps): React.JSX.Element {
|
}: ModuleListViewProps): React.JSX.Element {
|
||||||
const [moduleFilter, setModuleFilter] = useState<ModuleFilter>('all')
|
const [moduleFilter, setModuleFilter] = useState<ModuleFilter>('all')
|
||||||
const [deleteConfirmModule, setDeleteConfirmModule] = useState<ModuleRow | null>(null)
|
const [deleteConfirmModule, setDeleteConfirmModule] = useState<ModuleRow | null>(null)
|
||||||
const [parameterDialog, setParameterDialog] = useState<{ module: ModuleRow; parameters: ModuleParameterPair[] } | null>(null)
|
const [parameterDialog, setParameterDialog] = useState<{ module: ModuleRow; parameters: ModuleParameterPair[] } | null>(null)
|
||||||
const [moduleAddDialogOpen, setModuleAddDialogOpen] = useState(false)
|
const [moduleAddDialogOpen, setModuleAddDialogOpen] = useState(false)
|
||||||
|
const [moduleImportDialogOpen, setModuleImportDialogOpen] = useState(false)
|
||||||
const [moduleGuideOpen, setModuleGuideOpen] = useState(false)
|
const [moduleGuideOpen, setModuleGuideOpen] = useState(false)
|
||||||
const [floatingActionMenuOpen, setFloatingActionMenuOpen] = useState(false)
|
const [floatingActionMenuOpen, setFloatingActionMenuOpen] = useState(false)
|
||||||
const floatingActionMenuRef = useRef<HTMLDivElement>(null)
|
const floatingActionMenuRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -101,6 +104,7 @@ export default function ModuleListView({
|
|||||||
const stoppedCount = modules.filter((row) => row.state === 'stopped').length
|
const stoppedCount = modules.filter((row) => row.state === 'stopped').length
|
||||||
const installingAll = installingModuleName === '__all__'
|
const installingAll = installingModuleName === '__all__'
|
||||||
const bulkControlling = installingModuleName === '__start_all__' || installingModuleName === '__stop_all__'
|
const bulkControlling = installingModuleName === '__start_all__' || installingModuleName === '__stop_all__'
|
||||||
|
const hasModuleRows = modules.length > 0 || parkedModules.length > 0
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
@@ -234,11 +238,15 @@ export default function ModuleListView({
|
|||||||
setModuleAddDialogOpen(false)
|
setModuleAddDialogOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (moduleImportDialogOpen) {
|
||||||
|
event.preventDefault()
|
||||||
|
setModuleImportDialogOpen(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', handleEscape)
|
window.addEventListener('keydown', handleEscape)
|
||||||
return () => window.removeEventListener('keydown', handleEscape)
|
return () => window.removeEventListener('keydown', handleEscape)
|
||||||
}, [deleteConfirmModule, moduleAddDialogOpen, parameterDialog])
|
}, [deleteConfirmModule, moduleAddDialogOpen, moduleImportDialogOpen, parameterDialog])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={['relative flex h-full min-h-0 flex-col', dark ? 'bg-[#1f1f1f]' : 'bg-[#f7f8fb]'].join(' ')}>
|
<section className={['relative flex h-full min-h-0 flex-col', dark ? 'bg-[#1f1f1f]' : 'bg-[#f7f8fb]'].join(' ')}>
|
||||||
@@ -306,7 +314,7 @@ export default function ModuleListView({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={[dark ? 'divide-y divide-white/7' : 'divide-y divide-[#eef1f6]', 'flex min-h-full flex-col'].join(' ')}>
|
<div className={[dark ? 'divide-y divide-white/7' : 'divide-y divide-[#eef1f6]', 'flex min-h-full flex-col'].join(' ')}>
|
||||||
{loading ? (
|
{loading && !hasModuleRows ? (
|
||||||
<ModuleListEmptyState dark={dark} loading />
|
<ModuleListEmptyState dark={dark} loading />
|
||||||
) : filteredModuleRows.length === 0 ? (
|
) : filteredModuleRows.length === 0 ? (
|
||||||
<ModuleListEmptyState dark={dark} />
|
<ModuleListEmptyState dark={dark} />
|
||||||
@@ -381,12 +389,36 @@ export default function ModuleListView({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="모듈 추가"
|
aria-label="새 모듈"
|
||||||
disabled={!allowCustomModuleCreation}
|
disabled={!allowCustomModuleCreation}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModuleAddDialogOpen(true)
|
setModuleAddDialogOpen(true)
|
||||||
setFloatingActionMenuOpen(false)
|
setFloatingActionMenuOpen(false)
|
||||||
}}
|
}}
|
||||||
|
className={[
|
||||||
|
'absolute bottom-[180px] left-1/2 grid h-12 w-12 -translate-x-1/2 place-items-center rounded-full transition-all duration-200 ease-out',
|
||||||
|
allowCustomModuleCreation
|
||||||
|
? 'cursor-pointer bg-[#2563eb] text-white shadow-[0_8px_20px_rgba(20,54,104,0.24)] hover:scale-105 hover:bg-[#1d4ed8]'
|
||||||
|
: 'cursor-not-allowed bg-[#3b4250] text-white/35 shadow-none',
|
||||||
|
floatingActionMenuOpen ? 'pointer-events-auto translate-y-0 scale-100 opacity-100 delay-100' : 'pointer-events-none translate-y-4 scale-50 opacity-0'
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<PackagePlus size={18} />
|
||||||
|
<span className={[
|
||||||
|
'pointer-events-none absolute left-[calc(100%+8px)] whitespace-nowrap rounded-md px-2 py-1 text-[11px] font-bold shadow-[0_6px_18px_rgba(20,54,104,0.2)]',
|
||||||
|
allowCustomModuleCreation ? 'bg-[#2563eb] text-white' : 'bg-[#3b4250] text-white/35 shadow-none'
|
||||||
|
].join(' ')}>
|
||||||
|
새 모듈
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="모듈 불러오기"
|
||||||
|
disabled={!allowCustomModuleCreation}
|
||||||
|
onClick={() => {
|
||||||
|
setModuleImportDialogOpen(true)
|
||||||
|
setFloatingActionMenuOpen(false)
|
||||||
|
}}
|
||||||
className={[
|
className={[
|
||||||
'absolute bottom-[124px] left-1/2 grid h-12 w-12 -translate-x-1/2 place-items-center rounded-full transition-all duration-200 ease-out',
|
'absolute bottom-[124px] left-1/2 grid h-12 w-12 -translate-x-1/2 place-items-center rounded-full transition-all duration-200 ease-out',
|
||||||
allowCustomModuleCreation
|
allowCustomModuleCreation
|
||||||
@@ -395,12 +427,12 @@ export default function ModuleListView({
|
|||||||
floatingActionMenuOpen ? 'pointer-events-auto translate-y-0 scale-100 opacity-100 delay-75' : 'pointer-events-none translate-y-4 scale-50 opacity-0'
|
floatingActionMenuOpen ? 'pointer-events-auto translate-y-0 scale-100 opacity-100 delay-75' : 'pointer-events-none translate-y-4 scale-50 opacity-0'
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<PackagePlus size={18} />
|
<FolderOpen size={18} />
|
||||||
<span className={[
|
<span className={[
|
||||||
'pointer-events-none absolute left-[calc(100%+8px)] whitespace-nowrap rounded-md px-2 py-1 text-[11px] font-bold shadow-[0_6px_18px_rgba(20,54,104,0.2)]',
|
'pointer-events-none absolute left-[calc(100%+8px)] whitespace-nowrap rounded-md px-2 py-1 text-[11px] font-bold shadow-[0_6px_18px_rgba(20,54,104,0.2)]',
|
||||||
allowCustomModuleCreation ? 'bg-[#2563eb] text-white' : 'bg-[#3b4250] text-white/35 shadow-none'
|
allowCustomModuleCreation ? 'bg-[#2563eb] text-white' : 'bg-[#3b4250] text-white/35 shadow-none'
|
||||||
].join(' ')}>
|
].join(' ')}>
|
||||||
모듈 추가
|
모듈 불러오기
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -439,6 +471,16 @@ export default function ModuleListView({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{allowCustomModuleCreation && moduleImportDialogOpen && (
|
||||||
|
<ModuleImportDialog
|
||||||
|
dark={dark}
|
||||||
|
onClose={() => setModuleImportDialogOpen(false)}
|
||||||
|
onImport={async (input) => {
|
||||||
|
await onImportCustomModule(input)
|
||||||
|
setModuleImportDialogOpen(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{moduleGuideOpen && (
|
{moduleGuideOpen && (
|
||||||
<ModuleGuideDialog
|
<ModuleGuideDialog
|
||||||
dark={dark}
|
dark={dark}
|
||||||
@@ -616,6 +658,10 @@ export type CustomModuleInput = {
|
|||||||
launchFile: string
|
launchFile: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CustomModuleImportInput = CustomModuleInput & {
|
||||||
|
sourcePath: string
|
||||||
|
}
|
||||||
|
|
||||||
function ModuleAddDialog({
|
function ModuleAddDialog({
|
||||||
dark,
|
dark,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -665,7 +711,7 @@ function ModuleAddDialog({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-50 grid place-items-center bg-black/36 p-4" role="dialog" aria-modal="true" aria-label="모듈 정보 입력">
|
<div className="fixed inset-0 z-[200] grid place-items-center bg-black/36 p-4" role="dialog" aria-modal="true" aria-label="모듈 정보 입력">
|
||||||
<section className={['w-full max-w-[440px] overflow-hidden rounded-lg border shadow-[0_24px_70px_rgba(0,0,0,0.42)]', dark ? 'border-white/10 bg-[#252525] text-white' : 'border-[#d8dee9] bg-white text-[#111520]'].join(' ')}>
|
<section className={['w-full max-w-[440px] overflow-hidden rounded-lg border shadow-[0_24px_70px_rgba(0,0,0,0.42)]', dark ? 'border-white/10 bg-[#252525] text-white' : 'border-[#d8dee9] bg-white text-[#111520]'].join(' ')}>
|
||||||
<header className={['flex items-center justify-between border-b px-4 py-3', dark ? 'border-white/10' : 'border-[#d8dde8]'].join(' ')}>
|
<header className={['flex items-center justify-between border-b px-4 py-3', dark ? 'border-white/10' : 'border-[#d8dde8]'].join(' ')}>
|
||||||
<div>
|
<div>
|
||||||
@@ -749,3 +795,188 @@ function ModuleAddDialog({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ModuleImportDialog({
|
||||||
|
dark,
|
||||||
|
onClose,
|
||||||
|
onImport
|
||||||
|
}: {
|
||||||
|
dark: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onImport: (input: CustomModuleImportInput) => Promise<void>
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const directDockerImageOption = '__direct_input__'
|
||||||
|
const [source, setSource] = useState<{
|
||||||
|
sourcePath: string
|
||||||
|
packageName: string
|
||||||
|
launchFiles: string[]
|
||||||
|
buildType: 'ament_python' | 'ament_cmake'
|
||||||
|
} | null>(null)
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [category, setCategory] = useState('')
|
||||||
|
const [dockerImage, setDockerImage] = useState('')
|
||||||
|
const [dockerImageSelection, setDockerImageSelection] = useState('')
|
||||||
|
const [dockerImages, setDockerImages] = useState<string[]>([])
|
||||||
|
const [dockerImagesLoading, setDockerImagesLoading] = useState(true)
|
||||||
|
const [dockerImagesMessage, setDockerImagesMessage] = useState<string | null>(null)
|
||||||
|
const [launchFile, setLaunchFile] = useState('')
|
||||||
|
const [selectingSource, setSelectingSource] = useState(false)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const inputClassName = [
|
||||||
|
'h-9 w-full rounded-md border px-2.5 text-[12px] font-medium outline-none transition',
|
||||||
|
dark ? 'border-white/12 bg-[#1f1f1f] text-white/82 placeholder:text-white/28 focus:border-[#2f8dff]/55' : 'border-[#cfd6e4] bg-white text-[#111520] placeholder:text-[#9aa3b2] focus:border-[#2f8dff]/55'
|
||||||
|
].join(' ')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
void window.localWorkspace.modules.listDockerImages()
|
||||||
|
.then(({ images, message }) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setDockerImages(images)
|
||||||
|
setDockerImagesMessage(message ?? null)
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setDockerImagesMessage(reason instanceof Error ? reason.message : '로컬 Docker 이미지를 불러오지 못했습니다.')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setDockerImagesLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const selectSource = (): void => {
|
||||||
|
if (selectingSource) return
|
||||||
|
setSelectingSource(true)
|
||||||
|
setError(null)
|
||||||
|
void window.localWorkspace.modules.selectImportSource()
|
||||||
|
.then((selectedSource) => {
|
||||||
|
if (!selectedSource) return
|
||||||
|
setSource(selectedSource)
|
||||||
|
setName(selectedSource.packageName)
|
||||||
|
setLaunchFile(selectedSource.launchFiles[0] ?? '')
|
||||||
|
})
|
||||||
|
.catch((reason) => setError(reason instanceof Error ? reason.message : '모듈 구조를 확인하지 못했습니다.'))
|
||||||
|
.finally(() => setSelectingSource(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[200] grid place-items-center bg-black/36 p-4" role="dialog" aria-modal="true" aria-label="모듈 불러오기">
|
||||||
|
<section className={['w-full max-w-[480px] overflow-hidden rounded-lg border shadow-[0_24px_70px_rgba(0,0,0,0.42)]', dark ? 'border-white/10 bg-[#252525] text-white' : 'border-[#d8dee9] bg-white text-[#111520]'].join(' ')}>
|
||||||
|
<header className={['flex items-center justify-between border-b px-4 py-3', dark ? 'border-white/10' : 'border-[#d8dde8]'].join(' ')}>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[14px] font-bold">모듈 불러오기</h2>
|
||||||
|
<p className={['mt-1 text-[11px] font-medium', dark ? 'text-white/42' : 'text-[#727b8c]'].join(' ')}>ROS2 패키지 소스를 검증한 뒤 현재 워크스페이스로 복사합니다.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} className={['grid h-7 w-7 cursor-pointer place-items-center rounded-md transition', dark ? 'text-white/46 hover:bg-white/8 hover:text-white' : 'text-[#7b8494] hover:bg-[#eef1f7] hover:text-[#111520]'].join(' ')} aria-label="닫기">
|
||||||
|
<X size={15} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<form className="grid gap-3 p-4" onSubmit={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!source || submitting) return
|
||||||
|
setSubmitting(true)
|
||||||
|
setError(null)
|
||||||
|
void onImport({
|
||||||
|
name: name.trim(),
|
||||||
|
category,
|
||||||
|
dockerImage: dockerImage.trim(),
|
||||||
|
packageName: source.packageName,
|
||||||
|
launchFile,
|
||||||
|
sourcePath: source.sourcePath,
|
||||||
|
})
|
||||||
|
.catch((reason) => setError(reason instanceof Error ? reason.message : '모듈을 불러오지 못했습니다.'))
|
||||||
|
.finally(() => setSubmitting(false))
|
||||||
|
}}>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
<span className={['text-[11px] font-bold', dark ? 'text-white/58' : 'text-[#596477]'].join(' ')}>모듈 경로</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className={['w-0 min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap rounded-md border px-2.5 py-2 text-[11px] font-medium', dark ? 'border-white/12 bg-[#1f1f1f] text-white/66' : 'border-[#cfd6e4] bg-[#f8fafc] text-[#596477]'].join(' ')} title={source?.sourcePath}>
|
||||||
|
{source?.sourcePath ?? '선택한 폴더 없음'}
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={selectSource} disabled={selectingSource || submitting} className="inline-flex h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-md border border-[#2563eb]/50 px-2.5 text-[12px] font-bold text-[#2f8dff] transition hover:bg-[#2563eb]/10 disabled:cursor-not-allowed disabled:opacity-45">
|
||||||
|
<FolderOpen size={14} aria-hidden="true" />
|
||||||
|
{selectingSource ? '확인 중' : '경로 선택'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{source && (
|
||||||
|
<div className={['grid grid-cols-[1fr_auto] gap-x-3 gap-y-1 rounded-md border px-3 py-2 text-[11px] font-medium', dark ? 'border-[#2f8dff]/25 bg-[#2563eb]/10 text-white/74' : 'border-[#b7d3ff] bg-[#f2f7ff] text-[#40516b]'].join(' ')}>
|
||||||
|
<span>패키지: <strong className={dark ? 'text-white' : 'text-[#173c70]'}>{source.packageName}</strong></span>
|
||||||
|
<span>빌드: <strong className={dark ? 'text-white' : 'text-[#173c70]'}>{source.buildType}</strong></span>
|
||||||
|
<span className="col-span-2">Launch: {source.launchFiles.join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<label className={['grid gap-1 text-[11px] font-bold', dark ? 'text-white/58' : 'text-[#596477]'].join(' ')}>
|
||||||
|
모듈 이름
|
||||||
|
<input value={name} onChange={(event) => setName(event.target.value)} className={inputClassName} placeholder="예: NAV2-MyPlanner" disabled={!source} autoFocus />
|
||||||
|
</label>
|
||||||
|
<label className={['grid gap-1 text-[11px] font-bold', dark ? 'text-white/58' : 'text-[#596477]'].join(' ')}>
|
||||||
|
카테고리
|
||||||
|
<select value={category} onChange={(event) => setCategory(event.target.value)} className={inputClassName} disabled={!source}>
|
||||||
|
<option value="">선택</option>
|
||||||
|
<option value="Vehicle">Vehicle</option>
|
||||||
|
<option value="Sensing">Sensing</option>
|
||||||
|
<option value="Map">Map</option>
|
||||||
|
<option value="Perception">Perception</option>
|
||||||
|
<option value="Localization">Localization</option>
|
||||||
|
<option value="Planning">Planning</option>
|
||||||
|
<option value="Control">Control</option>
|
||||||
|
<option value="System">System</option>
|
||||||
|
<option value="Simulator">Simulator</option>
|
||||||
|
<option value="Gui">Gui</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className={['grid gap-1 text-[11px] font-bold', dark ? 'text-white/58' : 'text-[#596477]'].join(' ')}>
|
||||||
|
실행 이미지
|
||||||
|
<select
|
||||||
|
value={dockerImageSelection}
|
||||||
|
disabled={!source}
|
||||||
|
onChange={(event) => {
|
||||||
|
const selection = event.target.value
|
||||||
|
setDockerImageSelection(selection)
|
||||||
|
setDockerImage(selection === directDockerImageOption ? '' : selection)
|
||||||
|
}}
|
||||||
|
className={inputClassName}
|
||||||
|
>
|
||||||
|
<option value="" disabled>선택</option>
|
||||||
|
<option value={directDockerImageOption}>직접 입력</option>
|
||||||
|
{dockerImagesLoading && <option value="__loading__" disabled>로컬 Docker 이미지 불러오는 중</option>}
|
||||||
|
{dockerImages.map((image) => <option key={image} value={image}>{image}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{dockerImageSelection === directDockerImageOption && (
|
||||||
|
<label className={['grid gap-1 text-[11px] font-bold', dark ? 'text-white/58' : 'text-[#596477]'].join(' ')}>
|
||||||
|
직접 입력
|
||||||
|
<input value={dockerImage} onChange={(event) => setDockerImage(event.target.value)} className={inputClassName} placeholder="예: future-nav2:humble" disabled={!source} />
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{dockerImagesMessage && <p className={dark ? 'text-[11px] font-medium text-white/42' : 'text-[11px] font-medium text-[#7b8494]'}>{dockerImagesMessage}</p>}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<label className={['grid gap-1 text-[11px] font-bold', dark ? 'text-white/58' : 'text-[#596477]'].join(' ')}>
|
||||||
|
패키지명
|
||||||
|
<input value={source?.packageName ?? ''} readOnly className={[inputClassName, 'cursor-default opacity-70'].join(' ')} />
|
||||||
|
</label>
|
||||||
|
<label className={['grid gap-1 text-[11px] font-bold', dark ? 'text-white/58' : 'text-[#596477]'].join(' ')}>
|
||||||
|
Launch 파일
|
||||||
|
<select value={launchFile} onChange={(event) => setLaunchFile(event.target.value)} className={inputClassName} disabled={!source}>
|
||||||
|
<option value="" disabled>선택</option>
|
||||||
|
{source?.launchFiles.map((file) => <option key={file} value={file}>{file}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{error && <p className={dark ? 'text-[11px] font-semibold text-[#ff9c9c]' : 'text-[11px] font-semibold text-[#b42323]'}>{error}</p>}
|
||||||
|
<footer className="mt-1 flex justify-end gap-2">
|
||||||
|
<button type="button" disabled={submitting} onClick={onClose} className={['h-8 cursor-pointer rounded-md border px-3 text-[12px] font-bold transition disabled:cursor-not-allowed', dark ? 'border-white/12 text-white/62 hover:bg-white/8 hover:text-white' : 'border-[#cfd6e4] text-[#687284] hover:bg-[#eef1f7] hover:text-[#111520]'].join(' ')}>취소</button>
|
||||||
|
<button type="submit" disabled={submitting || !source || !name.trim() || !category || !launchFile} className="h-8 cursor-pointer rounded-md bg-[#2563eb] px-3 text-[12px] font-bold text-white transition hover:bg-[#1d4ed8] disabled:cursor-not-allowed disabled:opacity-45">{submitting ? '불러오는 중' : '확인'}</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type ModuleActionResult = {
|
|||||||
type SavedMapPathOverrides = {
|
type SavedMapPathOverrides = {
|
||||||
savedMapYamlPath?: string | null
|
savedMapYamlPath?: string | null
|
||||||
savedMapDatabasePath?: string | null
|
savedMapDatabasePath?: string | null
|
||||||
|
savedMapWorldLayoutPath?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const startupCategoryOrder = ['simulator', 'vehicle', 'sensing', 'map', 'localization', 'planning', 'control', 'system']
|
const startupCategoryOrder = ['simulator', 'vehicle', 'sensing', 'map', 'localization', 'planning', 'control', 'system']
|
||||||
@@ -142,14 +143,21 @@ export function useWorkspaceModuleControl({
|
|||||||
bulkExcludedModuleKeys.includes(module.folderName ?? module.name)
|
bulkExcludedModuleKeys.includes(module.folderName ?? module.name)
|
||||||
), [bulkExcludedModuleKeys])
|
), [bulkExcludedModuleKeys])
|
||||||
|
|
||||||
const getModuleRosParams = useCallback((module: ModuleRow, overrides?: { simulatorWorldFile?: string | null; savedMapYamlPath?: string | null; savedMapDatabasePath?: string | null }): string => {
|
const getModuleRosParams = useCallback((module: ModuleRow, overrides?: {
|
||||||
|
simulatorWorldFile?: string | null
|
||||||
|
savedMapYamlPath?: string | null
|
||||||
|
savedMapDatabasePath?: string | null
|
||||||
|
savedMapWorldLayoutPath?: string | null
|
||||||
|
}): string => {
|
||||||
const worldFile = overrides?.simulatorWorldFile ?? simulatorWorldFile
|
const worldFile = overrides?.simulatorWorldFile ?? simulatorWorldFile
|
||||||
const mapYamlPath = overrides?.savedMapYamlPath ?? savedMapYamlPath
|
const mapYamlPath = overrides?.savedMapYamlPath ?? savedMapYamlPath
|
||||||
const mapDatabasePath = overrides?.savedMapDatabasePath ?? savedMapDatabasePath
|
const mapDatabasePath = overrides?.savedMapDatabasePath ?? savedMapDatabasePath
|
||||||
|
const mapWorldLayoutPath = overrides?.savedMapWorldLayoutPath
|
||||||
const isMkMiniModule = /lms-nav2-mkmini/i.test(module.folderName ?? '')
|
const isMkMiniModule = /lms-nav2-mkmini/i.test(module.folderName ?? '')
|
||||||
const mkMiniProfile = vehicleRuntimeProfiles.find((profile) => profile.profileKey === 'mkmini')
|
const mkMiniProfile = vehicleRuntimeProfiles.find((profile) => profile.profileKey === 'mkmini')
|
||||||
const parameters = module.parameters.filter((parameter) => (
|
const parameters = module.parameters.filter((parameter) => (
|
||||||
!parameter.startsWith('world:=')
|
!parameter.startsWith('world:=')
|
||||||
|
&& !parameter.startsWith('world_layout_path:=')
|
||||||
&& !parameter.startsWith('world_layout_enabled:=')
|
&& !parameter.startsWith('world_layout_enabled:=')
|
||||||
&& !parameter.startsWith('map:=')
|
&& !parameter.startsWith('map:=')
|
||||||
&& !parameter.startsWith('database_path:=')
|
&& !parameter.startsWith('database_path:=')
|
||||||
@@ -176,6 +184,9 @@ export function useWorkspaceModuleControl({
|
|||||||
}
|
}
|
||||||
if (module.category.toLowerCase() === 'simulator' && worldFile) {
|
if (module.category.toLowerCase() === 'simulator' && worldFile) {
|
||||||
parameters.push(`world:=${worldFile}`)
|
parameters.push(`world:=${worldFile}`)
|
||||||
|
if (mapWorldLayoutPath) {
|
||||||
|
parameters.push(`world_layout_path:=${mapWorldLayoutPath.replace(/^\/workspace\//, '/modules/')}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (module.category.toLowerCase() === 'map') {
|
if (module.category.toLowerCase() === 'map') {
|
||||||
parameters.push(`world_layout_enabled:=${worldFile === 'empty_world.world'}`)
|
parameters.push(`world_layout_enabled:=${worldFile === 'empty_world.world'}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user