LUI添加3D
This commit is contained in:
parent
3ee2be723d
commit
2326c26305
@ -79,17 +79,17 @@ Size=93,65
|
||||
Collapsed=0
|
||||
|
||||
[Window][新建项目]
|
||||
Pos=725,358
|
||||
Pos=760,371
|
||||
Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择路径]
|
||||
Pos=625,258
|
||||
Pos=660,271
|
||||
Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][打开项目]
|
||||
Pos=675,308
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
Collapsed=0
|
||||
|
||||
|
||||
@ -412,9 +412,11 @@ class WebGLPackager:
|
||||
if entry:
|
||||
nodes_json.append(entry)
|
||||
|
||||
lui_manifest = self._build_lui_manifest()
|
||||
|
||||
manifest = {
|
||||
"meta": {
|
||||
"format_version": "1.0",
|
||||
"format_version": "1.1",
|
||||
"api_version": 1,
|
||||
"project_name": project_name,
|
||||
"exported_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
@ -430,8 +432,299 @@ class WebGLPackager:
|
||||
"environment": self._build_environment_entry(),
|
||||
"nodes": nodes_json,
|
||||
}
|
||||
if lui_manifest:
|
||||
manifest["lui"] = lui_manifest
|
||||
return manifest
|
||||
|
||||
def _build_lui_manifest(self) -> Dict[str, Any]:
|
||||
"""Serialize scene-space LUI canvases for the WebGL viewer."""
|
||||
lui_manager = getattr(self.world, "lui_manager", None)
|
||||
if not lui_manager:
|
||||
return {}
|
||||
|
||||
canvases = list(getattr(lui_manager, "canvases", []) or [])
|
||||
components = list(getattr(lui_manager, "components", []) or [])
|
||||
if not canvases:
|
||||
return {}
|
||||
|
||||
canvas_entries: List[Dict[str, Any]] = []
|
||||
for canvas_index, canvas in enumerate(canvases):
|
||||
if not isinstance(canvas, dict):
|
||||
continue
|
||||
render_mode = str(canvas.get("render_mode") or "screen").lower().strip()
|
||||
if render_mode != "world":
|
||||
continue
|
||||
|
||||
entry = self._build_lui_canvas_entry(canvas_index, canvas, components)
|
||||
if entry:
|
||||
canvas_entries.append(entry)
|
||||
|
||||
if not canvas_entries:
|
||||
return {}
|
||||
return {
|
||||
"version": 1,
|
||||
"source": "lui_manager",
|
||||
"canvases": canvas_entries,
|
||||
}
|
||||
|
||||
def _build_lui_canvas_entry(
|
||||
self,
|
||||
canvas_index: int,
|
||||
canvas: Dict[str, Any],
|
||||
components: List[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
mat = self._lui_canvas_matrix(canvas)
|
||||
width, height = self._lui_canvas_size(canvas)
|
||||
|
||||
component_entries = []
|
||||
for comp_index, comp in enumerate(components):
|
||||
if not isinstance(comp, dict):
|
||||
continue
|
||||
try:
|
||||
if int(comp.get("canvas_index", -1)) != canvas_index:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
entry = self._build_lui_component_entry(comp_index, comp, components)
|
||||
if entry:
|
||||
component_entries.append(entry)
|
||||
|
||||
component_entries.sort(
|
||||
key=lambda item: (
|
||||
self._safe_float(item.get("sort"), 1.0) + self._safe_float(item.get("z_offset"), 0.0),
|
||||
int(item.get("index", 0)),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"id": f"lui_canvas_{canvas_index}",
|
||||
"name": str(canvas.get("name") or f"Canvas {canvas_index + 1}"),
|
||||
"render_mode": "world",
|
||||
"visible": bool(canvas.get("visible", True)),
|
||||
"matrix_local_row_major": self._mat4_to_row_major_list(mat) if mat is not None else self._identity_mat4_row_major(),
|
||||
"world_position": self._lui_triplet(canvas.get("world_position"), (0.0, 8.0, 2.0)),
|
||||
"world_hpr": self._lui_triplet(canvas.get("world_hpr"), (0.0, 0.0, 0.0)),
|
||||
"world_scale": self._lui_triplet(canvas.get("world_scale"), (1.0, 1.0, 1.0)),
|
||||
"width": width,
|
||||
"height": height,
|
||||
"world_pixels_per_unit": max(1.0, self._safe_float(canvas.get("world_pixels_per_unit"), 280.0)),
|
||||
"world_depth_test": bool(canvas.get("world_depth_test", True)),
|
||||
"world_interactive": bool(canvas.get("world_interactive", True)),
|
||||
"world_two_sided": bool(canvas.get("world_two_sided", True)),
|
||||
"background_color": self._lui_canvas_background_color(canvas),
|
||||
"title_text": self._lui_text_value(canvas.get("title_text") or self._lui_object_text(canvas.get("title_label"))),
|
||||
"title_visible": bool(canvas.get("title_visible", False)),
|
||||
"components": component_entries,
|
||||
}
|
||||
|
||||
def _build_lui_component_entry(
|
||||
self,
|
||||
comp_index: int,
|
||||
comp: Dict[str, Any],
|
||||
components: List[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
left, top = self._lui_component_abs_pos(comp_index, components)
|
||||
width = max(1.0, self._safe_float(comp.get("width"), 100.0))
|
||||
height = max(1.0, self._safe_float(comp.get("height"), 30.0))
|
||||
comp_type = str(comp.get("type") or "Component")
|
||||
|
||||
entry: Dict[str, Any] = {
|
||||
"index": comp_index,
|
||||
"type": comp_type,
|
||||
"name": str(comp.get("name") or f"{comp_type}_{comp_index}"),
|
||||
"left": left,
|
||||
"top": top,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"sort": self._safe_float(comp.get("sort"), 1.0),
|
||||
"z_offset": self._safe_float(comp.get("z_offset"), 0.0),
|
||||
"text": self._lui_text_value(comp.get("text") or self._lui_object_text(comp.get("object"))),
|
||||
"value": self._lui_json_scalar(comp.get("value", "")),
|
||||
"placeholder": str(comp.get("placeholder") or ""),
|
||||
"checked": bool(comp.get("checked", False)),
|
||||
"color": self._normalize_lui_color(comp.get("color"), (1.0, 1.0, 1.0, 1.0)),
|
||||
"text_color": self._normalize_lui_color(comp.get("text_color"), (1.0, 1.0, 1.0, 1.0)),
|
||||
"font_size": max(8.0, self._safe_float(comp.get("font_size"), 14.0)),
|
||||
"min_value": self._safe_float(comp.get("min_value", comp.get("min", 0.0)), 0.0),
|
||||
"max_value": self._safe_float(comp.get("max_value", comp.get("max", 100.0)), 100.0),
|
||||
"selected_option_id": self._lui_json_scalar(comp.get("selected_option_id")),
|
||||
"options": self._serialize_lui_options(comp.get("options")),
|
||||
}
|
||||
|
||||
texture_path = str(comp.get("texture_path") or comp.get("image_path") or "").strip()
|
||||
texture_abs = self._resolve_lui_asset_path(texture_path)
|
||||
if texture_abs:
|
||||
texture_uri = self._copy_asset_to_textures(texture_abs)
|
||||
if texture_uri:
|
||||
entry["texture_uri"] = texture_uri
|
||||
return entry
|
||||
|
||||
def _lui_canvas_matrix(self, canvas: Dict[str, Any]) -> Any:
|
||||
node = canvas.get("node")
|
||||
render = getattr(self.world, "render", None)
|
||||
if node is not None:
|
||||
try:
|
||||
if not hasattr(node, "isEmpty") or not node.isEmpty():
|
||||
if render is not None:
|
||||
return node.getMat(render)
|
||||
return node.getMat()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
from panda3d.core import NodePath
|
||||
|
||||
temp = NodePath("lui_web_export_canvas_transform")
|
||||
temp.setPos(*self._lui_triplet(canvas.get("world_position"), (0.0, 8.0, 2.0)))
|
||||
temp.setHpr(*self._lui_triplet(canvas.get("world_hpr"), (0.0, 0.0, 0.0)))
|
||||
temp.setScale(*self._lui_triplet(canvas.get("world_scale"), (1.0, 1.0, 1.0)))
|
||||
return temp.getMat()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _lui_canvas_size(self, canvas: Dict[str, Any]) -> Tuple[float, float]:
|
||||
panel = canvas.get("panel")
|
||||
width = self._safe_float(canvas.get("width"), 0.0)
|
||||
height = self._safe_float(canvas.get("height"), 0.0)
|
||||
if panel is not None:
|
||||
try:
|
||||
width = self._safe_float(getattr(panel, "width", width), width)
|
||||
height = self._safe_float(getattr(panel, "height", height), height)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if width <= 0 and hasattr(panel, "get_width"):
|
||||
width = self._safe_float(panel.get_width(), width)
|
||||
if height <= 0 and hasattr(panel, "get_height"):
|
||||
height = self._safe_float(panel.get_height(), height)
|
||||
except Exception:
|
||||
pass
|
||||
return max(1.0, width or 800.0), max(1.0, height or 600.0)
|
||||
|
||||
def _lui_canvas_background_color(self, canvas: Dict[str, Any]) -> List[float]:
|
||||
background = canvas.get("background")
|
||||
try:
|
||||
if background is not None and hasattr(background, "color"):
|
||||
return self._normalize_lui_color(background.color, (0.1, 0.1, 0.1, 0.65))
|
||||
except Exception:
|
||||
pass
|
||||
return self._normalize_lui_color(canvas.get("background_color"), (0.1, 0.1, 0.1, 0.65))
|
||||
|
||||
def _lui_component_abs_pos(
|
||||
self,
|
||||
comp_index: int,
|
||||
components: List[Dict[str, Any]],
|
||||
visited: Optional[set] = None,
|
||||
) -> Tuple[float, float]:
|
||||
if comp_index < 0 or comp_index >= len(components):
|
||||
return 0.0, 0.0
|
||||
if visited is None:
|
||||
visited = set()
|
||||
if comp_index in visited:
|
||||
return 0.0, 0.0
|
||||
visited.add(comp_index)
|
||||
comp = components[comp_index]
|
||||
left = self._safe_float(comp.get("left"), 0.0)
|
||||
top = self._safe_float(comp.get("top"), 0.0)
|
||||
parent_index = comp.get("parent_index")
|
||||
try:
|
||||
parent_index_int = int(parent_index)
|
||||
except Exception:
|
||||
parent_index_int = -1
|
||||
if 0 <= parent_index_int < len(components):
|
||||
px, py = self._lui_component_abs_pos(parent_index_int, components, visited)
|
||||
return px + left, py + top
|
||||
return left, top
|
||||
|
||||
def _resolve_lui_asset_path(self, path_hint: str) -> Optional[str]:
|
||||
text = str(path_hint or "").strip()
|
||||
if not text or text.lower() == "blank":
|
||||
return None
|
||||
if text.startswith(("http://", "https://", "data:", "blob:")):
|
||||
return None
|
||||
if os.path.isabs(text) and os.path.exists(text):
|
||||
return os.path.normpath(text)
|
||||
roots = [
|
||||
self._project_path,
|
||||
os.path.join(self._project_path, "Resources"),
|
||||
os.path.join(self._project_path, "scenes", "resources"),
|
||||
os.getcwd(),
|
||||
]
|
||||
for root in roots:
|
||||
candidate = os.path.normpath(os.path.join(root, text))
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_lui_color(value: Any, fallback: Tuple[float, float, float, float]) -> List[float]:
|
||||
if not isinstance(value, (list, tuple)):
|
||||
value = fallback
|
||||
vals = list(value[:4])
|
||||
while len(vals) < 4:
|
||||
vals.append(1.0)
|
||||
out = []
|
||||
for idx, fallback_value in enumerate(fallback):
|
||||
try:
|
||||
num = float(vals[idx])
|
||||
except Exception:
|
||||
num = float(fallback_value)
|
||||
out.append(max(0.0, min(1.0, num)))
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _lui_triplet(value: Any, fallback: Tuple[float, float, float]) -> List[float]:
|
||||
if not isinstance(value, (list, tuple)) or len(value) < 3:
|
||||
value = fallback
|
||||
out = []
|
||||
for idx, fallback_value in enumerate(fallback):
|
||||
try:
|
||||
out.append(float(value[idx]))
|
||||
except Exception:
|
||||
out.append(float(fallback_value))
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _lui_text_value(value: Any) -> str:
|
||||
return "" if value is None else str(value)
|
||||
|
||||
@staticmethod
|
||||
def _lui_json_scalar(value: Any) -> Any:
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
@staticmethod
|
||||
def _lui_object_text(obj: Any) -> str:
|
||||
if obj is None:
|
||||
return ""
|
||||
for attr in ("text", "value", "label"):
|
||||
try:
|
||||
value = getattr(obj, attr, None)
|
||||
except Exception:
|
||||
value = None
|
||||
if value is not None:
|
||||
return str(value)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _serialize_lui_options(options: Any) -> List[Dict[str, Any]]:
|
||||
out: List[Dict[str, Any]] = []
|
||||
if not isinstance(options, (list, tuple)):
|
||||
return out
|
||||
for index, item in enumerate(options):
|
||||
if isinstance(item, dict):
|
||||
option_id = item.get("id", item.get("value", index))
|
||||
label = item.get("label", item.get("text", item.get("value", option_id)))
|
||||
elif isinstance(item, (list, tuple)) and item:
|
||||
option_id = item[0]
|
||||
label = item[1] if len(item) > 1 else item[0]
|
||||
else:
|
||||
option_id = index
|
||||
label = item
|
||||
out.append({"id": WebGLPackager._lui_json_scalar(option_id), "label": "" if label is None else str(label)})
|
||||
return out
|
||||
|
||||
def _collect_model_nodes(self) -> List[Any]:
|
||||
if not self.scene_manager:
|
||||
return []
|
||||
@ -2541,6 +2834,15 @@ class WebGLPackager:
|
||||
1.0,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _identity_mat4_row_major() -> List[float]:
|
||||
return [
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _safe_float(value: Any, default: float) -> float:
|
||||
try:
|
||||
|
||||
BIN
projects/新项目/Assets/Models/box1.glb
Normal file
BIN
projects/新项目/Assets/Models/box1.glb
Normal file
Binary file not shown.
8
projects/新项目/Assets/Models/box1.glb.meta
Normal file
8
projects/新项目/Assets/Models/box1.glb.meta
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"guid": "431e04e8b87e4e57b92549741c51ad45",
|
||||
"asset_type": "model",
|
||||
"source_hash": "fc694905c47a9b0005d77b701cc41852b56ef08c7406829a306e98f3ce158a64",
|
||||
"importer": "model_importer",
|
||||
"import_settings": {},
|
||||
"dependency_guids": []
|
||||
}
|
||||
502
projects/新项目/Scenes/Main.metascene
Normal file
502
projects/新项目/Scenes/Main.metascene
Normal file
@ -0,0 +1,502 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"scene_guid": "9810f0cbf25c4c02a8c6d514d14da9c2",
|
||||
"scene_name": "Main",
|
||||
"scene_file": "Scenes/Main.metascene",
|
||||
"saved_at": "2026-05-25 16:10:51",
|
||||
"cache": {
|
||||
"scene_bam": "",
|
||||
"gui_json": "Scenes/Main_gui.json",
|
||||
"lui_json": "Scenes/Main_lui.json"
|
||||
},
|
||||
"referenced_asset_guids": [
|
||||
"431e04e8b87e4e57b92549741c51ad45"
|
||||
],
|
||||
"referenced_script_guids": [],
|
||||
"scene_components": {
|
||||
"camera": {
|
||||
"position": [
|
||||
0.0,
|
||||
-50.0,
|
||||
20.0
|
||||
],
|
||||
"rotation": [
|
||||
6.041667938232422,
|
||||
-14.2271089553833,
|
||||
0.0
|
||||
],
|
||||
"camera_control_enabled": true
|
||||
},
|
||||
"gui": {
|
||||
"elements": []
|
||||
},
|
||||
"lui": {
|
||||
"canvases": [
|
||||
{
|
||||
"name": "1",
|
||||
"visible": true,
|
||||
"fill_mode": false,
|
||||
"fixed": false,
|
||||
"margins": {
|
||||
"left": 240.0,
|
||||
"right": 480.0,
|
||||
"top": 89.0,
|
||||
"bottom": 220.0
|
||||
},
|
||||
"render_mode": "world",
|
||||
"world_position": [
|
||||
-5.400000095367432,
|
||||
8.0,
|
||||
2.0
|
||||
],
|
||||
"world_hpr": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"world_scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"world_pixels_per_unit": 280.0,
|
||||
"world_depth_test": true,
|
||||
"world_interactive": true,
|
||||
"world_two_sided": true,
|
||||
"left": 240.0,
|
||||
"top": 89.0,
|
||||
"width": 1200.0,
|
||||
"height": 734.0,
|
||||
"background_color": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1.0
|
||||
],
|
||||
"title_visible": false,
|
||||
"title_text": "Canvas: 1",
|
||||
"title_pos": [
|
||||
10.0,
|
||||
10.0
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"type": "Button",
|
||||
"text": "New Button",
|
||||
"left": 550.0,
|
||||
"top": 352.0,
|
||||
"width": 100.0,
|
||||
"height": 30.0,
|
||||
"canvas_index": 0,
|
||||
"name": "Button_0",
|
||||
"color": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"sort": 1,
|
||||
"parent_index": null,
|
||||
"children_indices": []
|
||||
}
|
||||
],
|
||||
"current_canvas_index": 0,
|
||||
"selected_index": -1,
|
||||
"root_order": []
|
||||
}
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
0.0,
|
||||
-50.0,
|
||||
20.0
|
||||
],
|
||||
"rotation": [
|
||||
6.041667938232422,
|
||||
-14.2271089553833,
|
||||
0.0
|
||||
],
|
||||
"camera_control_enabled": true
|
||||
},
|
||||
"gui": [],
|
||||
"lui": {
|
||||
"canvases": [
|
||||
{
|
||||
"name": "1",
|
||||
"visible": true,
|
||||
"fill_mode": false,
|
||||
"fixed": false,
|
||||
"margins": {
|
||||
"left": 240.0,
|
||||
"right": 480.0,
|
||||
"top": 89.0,
|
||||
"bottom": 220.0
|
||||
},
|
||||
"render_mode": "world",
|
||||
"world_position": [
|
||||
-5.400000095367432,
|
||||
8.0,
|
||||
2.0
|
||||
],
|
||||
"world_hpr": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"world_scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"world_pixels_per_unit": 280.0,
|
||||
"world_depth_test": true,
|
||||
"world_interactive": true,
|
||||
"world_two_sided": true,
|
||||
"left": 240.0,
|
||||
"top": 89.0,
|
||||
"width": 1200.0,
|
||||
"height": 734.0,
|
||||
"background_color": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1.0
|
||||
],
|
||||
"title_visible": false,
|
||||
"title_text": "Canvas: 1",
|
||||
"title_pos": [
|
||||
10.0,
|
||||
10.0
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"type": "Button",
|
||||
"text": "New Button",
|
||||
"left": 550.0,
|
||||
"top": 352.0,
|
||||
"width": 100.0,
|
||||
"height": 30.0,
|
||||
"canvas_index": 0,
|
||||
"name": "Button_0",
|
||||
"color": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"sort": 1,
|
||||
"parent_index": null,
|
||||
"children_indices": []
|
||||
}
|
||||
],
|
||||
"current_canvas_index": 0,
|
||||
"selected_index": -1,
|
||||
"root_order": []
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": "0",
|
||||
"parent_id": null,
|
||||
"name": "box1.glb",
|
||||
"imported_node_key": "",
|
||||
"asset_guid": "431e04e8b87e4e57b92549741c51ad45",
|
||||
"asset_path": "Assets/Models/box1.glb",
|
||||
"node_class": "ModelRoot",
|
||||
"transform": {
|
||||
"position": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"rotation": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"visibility": {
|
||||
"user_visible": true
|
||||
},
|
||||
"runtime_interactive": false,
|
||||
"scripts": [],
|
||||
"components": {
|
||||
"model": {
|
||||
"asset_guid": "431e04e8b87e4e57b92549741c51ad45",
|
||||
"asset_path": "Assets/Models/box1.glb",
|
||||
"imported_node_key": "",
|
||||
"is_model_root": true
|
||||
},
|
||||
"scripts": {},
|
||||
"light": {},
|
||||
"material_overrides": {},
|
||||
"metadata": {
|
||||
"node_class": "ModelRoot",
|
||||
"runtime_interactive": false
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"model_path": "projects/新项目/Assets/Models/box1.glb",
|
||||
"saved_model_path": "projects/新项目/Assets/Models/box1.glb",
|
||||
"original_path": "projects/新项目/Assets/Models/box1.glb",
|
||||
"file": "box1.glb",
|
||||
"asset_guid": "431e04e8b87e4e57b92549741c51ad45",
|
||||
"asset_path": "Assets/Models/box1.glb",
|
||||
"is_model_root": "1",
|
||||
"is_scene_element": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": "0/0",
|
||||
"parent_id": "0",
|
||||
"name": "ROOT",
|
||||
"imported_node_key": "0/0",
|
||||
"asset_guid": "",
|
||||
"asset_path": "",
|
||||
"node_class": "PandaNode",
|
||||
"transform": {
|
||||
"position": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"rotation": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"visibility": {
|
||||
"user_visible": true
|
||||
},
|
||||
"runtime_interactive": false,
|
||||
"scripts": [],
|
||||
"components": {
|
||||
"model": {},
|
||||
"scripts": {},
|
||||
"light": {},
|
||||
"material_overrides": {},
|
||||
"metadata": {
|
||||
"node_class": "PandaNode",
|
||||
"runtime_interactive": false
|
||||
}
|
||||
},
|
||||
"tags": {}
|
||||
},
|
||||
{
|
||||
"node_id": "0/0/0",
|
||||
"parent_id": "0/0",
|
||||
"name": "立方体",
|
||||
"imported_node_key": "0/0/0",
|
||||
"asset_guid": "",
|
||||
"asset_path": "",
|
||||
"node_class": "GeomNode",
|
||||
"transform": {
|
||||
"position": [
|
||||
-1.6927927732467651,
|
||||
0.0,
|
||||
-4.459673881530762
|
||||
],
|
||||
"rotation": [
|
||||
-0.0,
|
||||
0.0,
|
||||
-0.0
|
||||
],
|
||||
"scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"visibility": {
|
||||
"user_visible": true
|
||||
},
|
||||
"runtime_interactive": false,
|
||||
"scripts": [],
|
||||
"components": {
|
||||
"model": {},
|
||||
"scripts": {},
|
||||
"light": {},
|
||||
"material_overrides": {},
|
||||
"metadata": {
|
||||
"node_class": "GeomNode",
|
||||
"runtime_interactive": false
|
||||
}
|
||||
},
|
||||
"tags": {}
|
||||
},
|
||||
{
|
||||
"node_id": "0/0/0/0",
|
||||
"parent_id": "0/0/0",
|
||||
"name": "立方体.001",
|
||||
"imported_node_key": "0/0/0/0",
|
||||
"asset_guid": "",
|
||||
"asset_path": "",
|
||||
"node_class": "GeomNode",
|
||||
"transform": {
|
||||
"position": [
|
||||
0.0,
|
||||
0.0,
|
||||
-3.55507755279541
|
||||
],
|
||||
"rotation": [
|
||||
-0.0,
|
||||
0.0,
|
||||
-0.0
|
||||
],
|
||||
"scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"visibility": {
|
||||
"user_visible": true
|
||||
},
|
||||
"runtime_interactive": false,
|
||||
"scripts": [],
|
||||
"components": {
|
||||
"model": {},
|
||||
"scripts": {},
|
||||
"light": {},
|
||||
"material_overrides": {
|
||||
"material_emission": "(0.0, 0.0, 0.0, 1.0)",
|
||||
"material_basecolor": "(0.800000011920929, 0.800000011920929, 0.800000011920929, 1.0)",
|
||||
"material_roughness": "0.5",
|
||||
"material_metallic": "0.0",
|
||||
"material_ior": "1.5",
|
||||
"material_render_effect_signature": "effects/default.yaml|0|1|0|0"
|
||||
},
|
||||
"metadata": {
|
||||
"node_class": "GeomNode",
|
||||
"runtime_interactive": false
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"Shadows": "3",
|
||||
"Voxelize": "3",
|
||||
"Envmap": "3",
|
||||
"material_render_effect_signature": "effects/default.yaml|0|1|0|0",
|
||||
"material_emission": "(0.0, 0.0, 0.0, 1.0)",
|
||||
"material_basecolor": "(0.800000011920929, 0.800000011920929, 0.800000011920929, 1.0)",
|
||||
"material_roughness": "0.5",
|
||||
"material_metallic": "0.0",
|
||||
"material_ior": "1.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": "0/0/0/0/0",
|
||||
"parent_id": "0/0/0/0",
|
||||
"name": "立方体.007",
|
||||
"imported_node_key": "0/0/0/0/0",
|
||||
"asset_guid": "",
|
||||
"asset_path": "",
|
||||
"node_class": "GeomNode",
|
||||
"transform": {
|
||||
"position": [
|
||||
4.175573348999023,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"rotation": [
|
||||
-0.0,
|
||||
0.0,
|
||||
-0.0
|
||||
],
|
||||
"scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"visibility": {
|
||||
"user_visible": true
|
||||
},
|
||||
"runtime_interactive": false,
|
||||
"scripts": [],
|
||||
"components": {
|
||||
"model": {},
|
||||
"scripts": {},
|
||||
"light": {},
|
||||
"material_overrides": {
|
||||
"material_emission": "(0.0, 0.0, 0.0, 1.0)",
|
||||
"material_basecolor": "(0.800000011920929, 0.800000011920929, 0.800000011920929, 1.0)",
|
||||
"material_roughness": "0.5",
|
||||
"material_metallic": "0.0",
|
||||
"material_ior": "1.5",
|
||||
"material_render_effect_signature": "effects/default.yaml|0|1|0|0"
|
||||
},
|
||||
"metadata": {
|
||||
"node_class": "GeomNode",
|
||||
"runtime_interactive": false
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"Shadows": "4",
|
||||
"Voxelize": "4",
|
||||
"Envmap": "4",
|
||||
"material_render_effect_signature": "effects/default.yaml|0|1|0|0",
|
||||
"material_emission": "(0.0, 0.0, 0.0, 1.0)",
|
||||
"material_basecolor": "(0.800000011920929, 0.800000011920929, 0.800000011920929, 1.0)",
|
||||
"material_roughness": "0.5",
|
||||
"material_metallic": "0.0",
|
||||
"material_ior": "1.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": "0/0/1",
|
||||
"parent_id": "0/0",
|
||||
"name": "立方体.003",
|
||||
"imported_node_key": "0/0/1",
|
||||
"asset_guid": "",
|
||||
"asset_path": "",
|
||||
"node_class": "GeomNode",
|
||||
"transform": {
|
||||
"position": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"rotation": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"scale": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"visibility": {
|
||||
"user_visible": true
|
||||
},
|
||||
"runtime_interactive": false,
|
||||
"scripts": [],
|
||||
"components": {
|
||||
"model": {},
|
||||
"scripts": {},
|
||||
"light": {},
|
||||
"material_overrides": {},
|
||||
"metadata": {
|
||||
"node_class": "GeomNode",
|
||||
"runtime_interactive": false
|
||||
}
|
||||
},
|
||||
"tags": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
49
projects/新项目/project.json
Normal file
49
projects/新项目/project.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "新项目",
|
||||
"path": "/home/hello/EG/projects/新项目",
|
||||
"created_at": "2026-05-25 16:08:52",
|
||||
"last_modified": "2026-05-25 16:10:51",
|
||||
"version": "2.0.0",
|
||||
"engine_name": "MetaCore",
|
||||
"engine_version": "2.0.0",
|
||||
"scenes": [
|
||||
{
|
||||
"guid": "9810f0cbf25c4c02a8c6d514d14da9c2",
|
||||
"name": "Main",
|
||||
"path": "Scenes/Main.metascene",
|
||||
"enabled_in_build": true
|
||||
}
|
||||
],
|
||||
"scene_file": "Scenes/Main.metascene",
|
||||
"startup_scene_guid": "9810f0cbf25c4c02a8c6d514d14da9c2",
|
||||
"last_open_scene_guid": "9810f0cbf25c4c02a8c6d514d14da9c2",
|
||||
"build_settings": {
|
||||
"enabled_scene_guids": [
|
||||
"9810f0cbf25c4c02a8c6d514d14da9c2"
|
||||
],
|
||||
"output_format": "directory",
|
||||
"windows_player": true,
|
||||
"active_profile": "default",
|
||||
"profiles": {
|
||||
"default": {
|
||||
"name": "Default",
|
||||
"target_platform": "windows",
|
||||
"output_format": "directory",
|
||||
"output_dir": "Builds",
|
||||
"windows": {
|
||||
"enabled": true,
|
||||
"exe_name": "",
|
||||
"windowed": true
|
||||
},
|
||||
"runtime": {
|
||||
"startup_scene_guid": "9810f0cbf25c4c02a8c6d514d14da9c2",
|
||||
"enabled_scene_guids": [
|
||||
"9810f0cbf25c4c02a8c6d514d14da9c2"
|
||||
],
|
||||
"script_mode": "pyc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -998,6 +998,516 @@ function applyShadowFlags(root, enabled) {
|
||||
});
|
||||
}
|
||||
|
||||
function luiClamp01(value, fallback = 1) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.max(0, Math.min(1, n));
|
||||
}
|
||||
|
||||
function luiColorToCss(color, fallback = [1, 1, 1, 1]) {
|
||||
const src = Array.isArray(color) ? color : fallback;
|
||||
const r = Math.round(luiClamp01(src[0], fallback[0] ?? 1) * 255);
|
||||
const g = Math.round(luiClamp01(src[1], fallback[1] ?? 1) * 255);
|
||||
const b = Math.round(luiClamp01(src[2], fallback[2] ?? 1) * 255);
|
||||
const a = luiClamp01(src[3], fallback[3] ?? 1);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
function luiRoundRect(ctx, x, y, w, h, r) {
|
||||
const radius = Math.max(0, Math.min(Number(r) || 0, Math.min(w, h) * 0.5));
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + w - radius, y);
|
||||
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
||||
ctx.lineTo(x + w, y + h - radius);
|
||||
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
||||
ctx.lineTo(x + radius, y + h);
|
||||
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function luiDrawText(ctx, text, x, y, width, height, options = {}) {
|
||||
const fontSize = Math.max(8, Number(options.fontSize ?? 14));
|
||||
ctx.font = `${fontSize}px ${options.fontFamily || "system-ui, -apple-system, Segoe UI, sans-serif"}`;
|
||||
ctx.fillStyle = options.color || "rgba(245,247,250,0.95)";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.textAlign = options.align || "left";
|
||||
const maxWidth = Math.max(1, Number(width) || 1);
|
||||
let tx = Number(x) || 0;
|
||||
if (ctx.textAlign === "center") tx += maxWidth * 0.5;
|
||||
if (ctx.textAlign === "right") tx += maxWidth;
|
||||
ctx.fillText(String(text ?? ""), tx, (Number(y) || 0) + Math.max(fontSize * 0.5, (Number(height) || fontSize) * 0.5), maxWidth);
|
||||
}
|
||||
|
||||
function luiSelectedOptionLabel(comp) {
|
||||
const options = Array.isArray(comp?.options) ? comp.options : [];
|
||||
if (options.length === 0) return "Select";
|
||||
const selected = comp?.selected_option_id;
|
||||
const found = options.find((item) => String(item?.id) === String(selected));
|
||||
return String((found || options[0])?.label ?? (found || options[0])?.id ?? "Select");
|
||||
}
|
||||
|
||||
function drawLUIComponent(ctx, comp, state) {
|
||||
const x = Number(comp.left ?? 0);
|
||||
const y = Number(comp.top ?? 0);
|
||||
const w = Math.max(1, Number(comp.width ?? 100));
|
||||
const h = Math.max(1, Number(comp.height ?? 30));
|
||||
const type = String(comp.type || "").toLowerCase();
|
||||
const hovered = state.hoveredIndex === comp.index;
|
||||
const pressed = state.pressedIndex === comp.index;
|
||||
const fontSize = Math.max(8, Number(comp.font_size ?? 14));
|
||||
|
||||
if (type === "text" || type === "label") {
|
||||
luiDrawText(ctx, comp.text || "", x, y, w, h, {
|
||||
fontSize,
|
||||
color: luiColorToCss(comp.color, [1, 1, 1, 1]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "button") {
|
||||
ctx.fillStyle = pressed ? "rgba(28,41,59,0.98)" : hovered ? "rgba(61,83,108,0.95)" : "rgba(46,64,86,0.92)";
|
||||
luiRoundRect(ctx, x, y, w, h, 4);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.28)";
|
||||
ctx.stroke();
|
||||
luiDrawText(ctx, comp.text || "Button", x + 8, y, w - 16, h, {
|
||||
align: "center",
|
||||
fontSize: 14,
|
||||
color: "rgba(245,247,250,0.96)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "checkbox") {
|
||||
const box = Math.min(18, h);
|
||||
const by = y + (h - box) * 0.5;
|
||||
ctx.fillStyle = "rgba(20,27,38,0.96)";
|
||||
ctx.fillRect(x, by, box, box);
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.35)";
|
||||
ctx.strokeRect(x, by, box, box);
|
||||
if (comp.checked) {
|
||||
ctx.strokeStyle = "rgba(125,255,205,1)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 4, by + box * 0.55);
|
||||
ctx.lineTo(x + box * 0.42, by + box - 4);
|
||||
ctx.lineTo(x + box - 3, by + 4);
|
||||
ctx.stroke();
|
||||
ctx.lineWidth = 1;
|
||||
}
|
||||
luiDrawText(ctx, comp.text || "Checkbox", x + box + 8, y, w - box - 8, h, {
|
||||
fontSize: 14,
|
||||
color: "rgba(245,247,250,0.96)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "slider") {
|
||||
const minV = Number(comp.min_value ?? 0);
|
||||
const maxV = Number(comp.max_value ?? 100);
|
||||
const val = Number(comp.value ?? ((minV + maxV) * 0.5));
|
||||
const ratio = maxV === minV ? 0 : Math.max(0, Math.min(1, (val - minV) / (maxV - minV)));
|
||||
const barH = Math.max(3, h * 0.16);
|
||||
const barY = y + h * 0.5 - barH * 0.5;
|
||||
ctx.fillStyle = "rgba(24,32,43,0.96)";
|
||||
ctx.fillRect(x, barY, w, barH);
|
||||
ctx.fillStyle = "rgba(115,210,180,0.96)";
|
||||
ctx.fillRect(x, barY, w * ratio, barH);
|
||||
const knob = Math.max(10, Math.min(18, h + 2));
|
||||
ctx.fillStyle = "rgba(238,244,255,1)";
|
||||
ctx.fillRect(x + w * ratio - knob * 0.5, y + (h - knob) * 0.5, knob, knob);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "inputfield") {
|
||||
ctx.fillStyle = "rgba(10,15,22,0.96)";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = state.focusedIndex === comp.index ? "rgba(125,210,185,0.86)" : "rgba(255,255,255,0.24)";
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
const hasText = String(comp.value ?? comp.text ?? "").length > 0;
|
||||
luiDrawText(ctx, hasText ? (comp.value ?? comp.text) : (comp.placeholder || ""), x + 8, y, w - 16, h, {
|
||||
fontSize: 14,
|
||||
color: hasText ? "rgba(245,247,250,0.96)" : "rgba(210,215,225,0.55)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "selectbox") {
|
||||
ctx.fillStyle = "rgba(10,15,22,0.96)";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.24)";
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
luiDrawText(ctx, luiSelectedOptionLabel(comp), x + 8, y, w - 30, h, {
|
||||
fontSize: 14,
|
||||
color: "rgba(245,247,250,0.96)",
|
||||
});
|
||||
luiDrawText(ctx, "v", x + w - 20, y, 14, h, {
|
||||
align: "center",
|
||||
fontSize: 14,
|
||||
color: "rgba(245,247,250,0.85)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "progressbar") {
|
||||
const ratio = Math.max(0, Math.min(1, Number(comp.value ?? 0) / 100));
|
||||
ctx.fillStyle = "rgba(16,21,28,0.96)";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.fillStyle = "rgba(82,194,158,0.96)";
|
||||
ctx.fillRect(x, y, w * ratio, h);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "image" && comp.imageElement && comp.imageElement.complete) {
|
||||
try {
|
||||
ctx.drawImage(comp.imageElement, x, y, w, h);
|
||||
return;
|
||||
} catch (err) {
|
||||
// Use fallback below when the image is not drawable.
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = luiColorToCss(comp.color, [0.12, 0.15, 0.19, 0.86]);
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.14)";
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
}
|
||||
|
||||
function drawLuiCanvas(luiCanvas) {
|
||||
const ctx = luiCanvas.context2d;
|
||||
const w = luiCanvas.pixelWidth;
|
||||
const h = luiCanvas.pixelHeight;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillStyle = luiColorToCss(luiCanvas.data.background_color, [0.1, 0.1, 0.1, 0.68]);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
if (luiCanvas.data.title_visible && luiCanvas.data.title_text) {
|
||||
luiDrawText(ctx, luiCanvas.data.title_text, 10, 10, w - 20, 24, {
|
||||
fontSize: 16,
|
||||
color: "rgba(220,226,235,0.90)",
|
||||
});
|
||||
}
|
||||
|
||||
for (const comp of luiCanvas.components || []) {
|
||||
drawLUIComponent(ctx, comp, luiCanvas);
|
||||
}
|
||||
if (luiCanvas.texture) luiCanvas.texture.needsUpdate = true;
|
||||
}
|
||||
|
||||
function createLuiCanvasObject(THREE, runtime, canvasData) {
|
||||
const width = Math.max(1, Math.round(Number(canvasData.width ?? 800)));
|
||||
const height = Math.max(1, Math.round(Number(canvasData.height ?? 600)));
|
||||
const ppu = Math.max(1, Number(canvasData.world_pixels_per_unit ?? 280));
|
||||
const htmlCanvas = document.createElement("canvas");
|
||||
htmlCanvas.width = width;
|
||||
htmlCanvas.height = height;
|
||||
const context2d = htmlCanvas.getContext("2d");
|
||||
if (!context2d) return null;
|
||||
|
||||
const texture = new THREE.CanvasTexture(htmlCanvas);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.wrapS = THREE.ClampToEdgeWrapping;
|
||||
texture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
texture.generateMipmaps = false;
|
||||
if ("colorSpace" in texture && THREE.SRGBColorSpace) texture.colorSpace = THREE.SRGBColorSpace;
|
||||
else if ("encoding" in texture && THREE.sRGBEncoding) texture.encoding = THREE.sRGBEncoding;
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
side: toBoolean(canvasData.world_two_sided, true) ? THREE.DoubleSide : THREE.FrontSide,
|
||||
depthTest: toBoolean(canvasData.world_depth_test, true),
|
||||
depthWrite: false,
|
||||
toneMapped: false,
|
||||
});
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(width / ppu, height / ppu), material);
|
||||
mesh.name = `${canvasData.name || canvasData.id || "LUI Canvas"} Mesh`;
|
||||
mesh.userData.luiCanvasId = canvasData.id || "";
|
||||
|
||||
const group = new THREE.Group();
|
||||
group.name = canvasData.name || canvasData.id || "LUI Canvas";
|
||||
group.visible = toBoolean(canvasData.visible, true);
|
||||
group.add(mesh);
|
||||
if (Array.isArray(canvasData.matrix_local_row_major) && canvasData.matrix_local_row_major.length === 16) {
|
||||
const converted = convertNodeMatrix(
|
||||
THREE,
|
||||
canvasData.matrix_local_row_major,
|
||||
runtime.basis,
|
||||
runtime.basisInv,
|
||||
runtime.matrixConvention,
|
||||
);
|
||||
group.matrixAutoUpdate = false;
|
||||
group.matrix.copy(converted);
|
||||
group.matrix.decompose(group.position, group.quaternion, group.scale);
|
||||
}
|
||||
|
||||
const components = (Array.isArray(canvasData.components) ? canvasData.components : [])
|
||||
.map((comp) => ({ ...comp }))
|
||||
.sort((a, b) => {
|
||||
const za = Number(a.sort ?? 1) + Number(a.z_offset ?? 0);
|
||||
const zb = Number(b.sort ?? 1) + Number(b.z_offset ?? 0);
|
||||
if (za !== zb) return za - zb;
|
||||
return Number(a.index ?? 0) - Number(b.index ?? 0);
|
||||
});
|
||||
|
||||
const luiCanvas = {
|
||||
data: canvasData,
|
||||
group,
|
||||
mesh,
|
||||
texture,
|
||||
htmlCanvas,
|
||||
context2d,
|
||||
pixelWidth: width,
|
||||
pixelHeight: height,
|
||||
components,
|
||||
hoveredIndex: -1,
|
||||
pressedIndex: -1,
|
||||
focusedIndex: -1,
|
||||
activeSliderIndex: -1,
|
||||
interactive: toBoolean(canvasData.world_interactive, true),
|
||||
};
|
||||
|
||||
for (const comp of components) {
|
||||
if (comp.texture_uri && String(comp.type || "").toLowerCase() === "image") {
|
||||
const img = new Image();
|
||||
img.onload = () => drawLuiCanvas(luiCanvas);
|
||||
img.onerror = () => drawLuiCanvas(luiCanvas);
|
||||
img.src = resolveAssetUrl(comp.texture_uri);
|
||||
comp.imageElement = img;
|
||||
}
|
||||
}
|
||||
|
||||
drawLuiCanvas(luiCanvas);
|
||||
return luiCanvas;
|
||||
}
|
||||
|
||||
function hitTestLuiComponent(luiCanvas, px, py) {
|
||||
const matches = [];
|
||||
for (const comp of luiCanvas.components || []) {
|
||||
const x = Number(comp.left ?? 0);
|
||||
const y = Number(comp.top ?? 0);
|
||||
const w = Math.max(1, Number(comp.width ?? 100));
|
||||
const h = Math.max(1, Number(comp.height ?? 30));
|
||||
if (px >= x && px <= x + w && py >= y && py <= y + h) matches.push(comp);
|
||||
}
|
||||
matches.sort((a, b) => {
|
||||
const za = Number(a.sort ?? 1) + Number(a.z_offset ?? 0);
|
||||
const zb = Number(b.sort ?? 1) + Number(b.z_offset ?? 0);
|
||||
if (za !== zb) return zb - za;
|
||||
return Number(b.index ?? 0) - Number(a.index ?? 0);
|
||||
});
|
||||
return matches[0] || null;
|
||||
}
|
||||
|
||||
function setLuiSliderFromHit(luiCanvas, comp, px) {
|
||||
if (!comp) return;
|
||||
const x = Number(comp.left ?? 0);
|
||||
const w = Math.max(1, Number(comp.width ?? 100));
|
||||
const minV = Number(comp.min_value ?? 0);
|
||||
const maxV = Number(comp.max_value ?? 100);
|
||||
const ratio = Math.max(0, Math.min(1, (px - x) / w));
|
||||
comp.value = minV + ratio * (maxV - minV);
|
||||
drawLuiCanvas(luiCanvas);
|
||||
}
|
||||
|
||||
function createWorldLuiRuntime(THREE, runtime) {
|
||||
const luiData = runtime?.manifest?.lui;
|
||||
const canvasDataList = Array.isArray(luiData?.canvases) ? luiData.canvases : [];
|
||||
const canvases = [];
|
||||
const meshToCanvas = new Map();
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const pointer = new THREE.Vector2();
|
||||
const active = { canvas: null, component: null, x: 0, y: 0 };
|
||||
const dom = runtime.renderer.domElement;
|
||||
|
||||
for (const canvasData of canvasDataList) {
|
||||
if (!canvasData || String(canvasData.render_mode || "").toLowerCase() !== "world") continue;
|
||||
const luiCanvas = createLuiCanvasObject(THREE, runtime, canvasData);
|
||||
if (!luiCanvas) continue;
|
||||
runtime.scene.add(luiCanvas.group);
|
||||
canvases.push(luiCanvas);
|
||||
meshToCanvas.set(luiCanvas.mesh, luiCanvas);
|
||||
}
|
||||
|
||||
const updatePointer = (event) => {
|
||||
const rect = dom.getBoundingClientRect();
|
||||
pointer.x = ((event.clientX - rect.left) / Math.max(1, rect.width)) * 2 - 1;
|
||||
pointer.y = -(((event.clientY - rect.top) / Math.max(1, rect.height)) * 2 - 1);
|
||||
};
|
||||
|
||||
const pick = (event) => {
|
||||
if (canvases.length === 0) return null;
|
||||
updatePointer(event);
|
||||
raycaster.setFromCamera(pointer, runtime.camera);
|
||||
const meshes = canvases.filter((c) => c.interactive && c.group.visible).map((c) => c.mesh);
|
||||
const hits = raycaster.intersectObjects(meshes, false);
|
||||
if (!hits.length) return null;
|
||||
const hit = hits[0];
|
||||
const luiCanvas = meshToCanvas.get(hit.object);
|
||||
if (!luiCanvas || !hit.uv) return null;
|
||||
const px = hit.uv.x * luiCanvas.pixelWidth;
|
||||
const py = (1 - hit.uv.y) * luiCanvas.pixelHeight;
|
||||
const component = hitTestLuiComponent(luiCanvas, px, py);
|
||||
return { canvas: luiCanvas, component, x: px, y: py };
|
||||
};
|
||||
|
||||
const emitLuiEvent = (eventName, hit, component) => emitRuntimeEvent(runtime, "luiEvent", {
|
||||
event: eventName,
|
||||
canvas: { id: hit?.canvas?.data?.id || "", name: hit?.canvas?.data?.name || "" },
|
||||
component: component ? {
|
||||
index: component.index,
|
||||
type: component.type || "",
|
||||
name: component.name || "",
|
||||
value: component.value,
|
||||
checked: !!component.checked,
|
||||
selectedOptionId: component.selected_option_id,
|
||||
} : null,
|
||||
pointer: hit ? { x: hit.x, y: hit.y } : null,
|
||||
});
|
||||
|
||||
const clearFocusedInput = () => {
|
||||
let changed = false;
|
||||
for (const luiCanvas of canvases) {
|
||||
if (luiCanvas.focusedIndex >= 0) {
|
||||
luiCanvas.focusedIndex = -1;
|
||||
drawLuiCanvas(luiCanvas);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
dom.addEventListener("pointermove", (event) => {
|
||||
const hit = pick(event);
|
||||
if (active.canvas && active.canvas.activeSliderIndex >= 0 && event.buttons) {
|
||||
const comp = active.canvas.components.find((c) => c.index === active.canvas.activeSliderIndex);
|
||||
setLuiSliderFromHit(active.canvas, comp, hit ? hit.x : active.x);
|
||||
return;
|
||||
}
|
||||
const nextCanvas = hit?.canvas || null;
|
||||
const nextIndex = hit?.component?.index ?? -1;
|
||||
for (const luiCanvas of canvases) {
|
||||
const wanted = luiCanvas === nextCanvas ? nextIndex : -1;
|
||||
if (luiCanvas.hoveredIndex !== wanted) {
|
||||
luiCanvas.hoveredIndex = wanted;
|
||||
drawLuiCanvas(luiCanvas);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onPointerDown = (event) => {
|
||||
const hit = pick(event);
|
||||
if (!hit) {
|
||||
clearFocusedInput();
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (typeof event.stopImmediatePropagation === "function") {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
try {
|
||||
if (event.pointerId !== undefined && typeof dom.setPointerCapture === "function") {
|
||||
dom.setPointerCapture(event.pointerId);
|
||||
}
|
||||
} catch (err) {
|
||||
// Pointer capture is best-effort; interaction still works without it.
|
||||
}
|
||||
if (runtime.controls) runtime.controls.enabled = false;
|
||||
active.canvas = hit.canvas;
|
||||
active.component = hit.component;
|
||||
active.x = hit.x;
|
||||
active.y = hit.y;
|
||||
const comp = hit.component;
|
||||
hit.canvas.pressedIndex = comp?.index ?? -1;
|
||||
hit.canvas.focusedIndex = String(comp?.type || "").toLowerCase() === "inputfield" ? comp.index : -1;
|
||||
hit.canvas.activeSliderIndex = String(comp?.type || "").toLowerCase() === "slider" ? comp.index : -1;
|
||||
if (hit.canvas.activeSliderIndex >= 0) setLuiSliderFromHit(hit.canvas, comp, hit.x);
|
||||
drawLuiCanvas(hit.canvas);
|
||||
emitLuiEvent("mousedown", hit, comp);
|
||||
};
|
||||
dom.addEventListener("pointerdown", onPointerDown, { capture: true });
|
||||
|
||||
const finishPointer = (event) => {
|
||||
if (!active.canvas) return;
|
||||
const startCanvas = active.canvas;
|
||||
const startComponent = active.component;
|
||||
const hit = pick(event);
|
||||
const releaseSame = hit && hit.canvas === startCanvas && hit.component === startComponent;
|
||||
const type = String(startComponent?.type || "").toLowerCase();
|
||||
if (releaseSame && startComponent) {
|
||||
if (type === "checkbox") {
|
||||
startComponent.checked = !toBoolean(startComponent.checked, false);
|
||||
emitLuiEvent("changed", hit, startComponent);
|
||||
} else if (type === "selectbox") {
|
||||
const options = Array.isArray(startComponent.options) ? startComponent.options : [];
|
||||
if (options.length > 0) {
|
||||
const ids = options.map((item) => item?.id);
|
||||
const cur = ids.findIndex((id) => String(id) === String(startComponent.selected_option_id));
|
||||
startComponent.selected_option_id = ids[(cur + 1 + ids.length) % ids.length];
|
||||
}
|
||||
emitLuiEvent("changed", hit, startComponent);
|
||||
} else if (type === "slider") {
|
||||
emitLuiEvent("changed", hit || active, startComponent);
|
||||
} else {
|
||||
emitLuiEvent("click", hit, startComponent);
|
||||
}
|
||||
}
|
||||
startCanvas.pressedIndex = -1;
|
||||
startCanvas.activeSliderIndex = -1;
|
||||
drawLuiCanvas(startCanvas);
|
||||
active.canvas = null;
|
||||
active.component = null;
|
||||
try {
|
||||
if (event?.pointerId !== undefined && typeof dom.releasePointerCapture === "function") {
|
||||
dom.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
} catch (err) {
|
||||
// The pointer may already be released by the browser.
|
||||
}
|
||||
if (runtime.controls) runtime.controls.enabled = true;
|
||||
};
|
||||
dom.addEventListener("pointerup", finishPointer);
|
||||
dom.addEventListener("pointercancel", finishPointer);
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
const focusedCanvas = canvases.find((c) => c.focusedIndex >= 0);
|
||||
if (!focusedCanvas) return;
|
||||
const comp = focusedCanvas.components.find((c) => c.index === focusedCanvas.focusedIndex);
|
||||
if (!comp || String(comp.type || "").toLowerCase() !== "inputfield") return;
|
||||
if (event.key === "Backspace") {
|
||||
comp.value = String(comp.value ?? comp.text ?? "").slice(0, -1);
|
||||
} else if (event.key === "Enter" || event.key === "Escape") {
|
||||
focusedCanvas.focusedIndex = -1;
|
||||
} else if (event.key.length === 1) {
|
||||
comp.value = String(comp.value ?? comp.text ?? "") + event.key;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
drawLuiCanvas(focusedCanvas);
|
||||
emitRuntimeEvent(runtime, "luiEvent", {
|
||||
event: "changed",
|
||||
canvas: { id: focusedCanvas.data.id || "", name: focusedCanvas.data.name || "" },
|
||||
component: { index: comp.index, type: comp.type || "", name: comp.name || "", value: comp.value },
|
||||
pointer: null,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
canvases,
|
||||
count: canvases.length,
|
||||
redraw() {
|
||||
for (const luiCanvas of canvases) drawLuiCanvas(luiCanvas);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeColorInput(color, fallback = [1, 1, 1]) {
|
||||
const out = toColorArray(color, fallback);
|
||||
if (out.some((v) => Number(v) > 1.0)) {
|
||||
@ -2772,6 +3282,16 @@ function buildSceneInfo(runtime) {
|
||||
nodeCount: runtime?.nodeOrder?.length || 0,
|
||||
unsupportedScriptCount: runtime?.unsupportedScriptCount || 0,
|
||||
webSupportedScripts: listWebSupportedScripts(),
|
||||
lui: {
|
||||
canvasCount: runtime?.lui?.count || 0,
|
||||
canvases: (runtime?.lui?.canvases || []).map((canvas) => ({
|
||||
id: canvas?.data?.id || "",
|
||||
name: canvas?.data?.name || "",
|
||||
visible: !!canvas?.group?.visible,
|
||||
interactive: !!canvas?.interactive,
|
||||
componentCount: canvas?.components?.length || 0,
|
||||
})),
|
||||
},
|
||||
nodes: (runtime?.nodeOrder || []).map((entry) => buildNodeSummary(entry)),
|
||||
};
|
||||
}
|
||||
@ -3381,6 +3901,7 @@ async function bootstrap() {
|
||||
unsupportedScriptCount: 0,
|
||||
isReady: false,
|
||||
bridge: null,
|
||||
lui: null,
|
||||
};
|
||||
activeViewerRuntime = runtime;
|
||||
|
||||
@ -3420,6 +3941,8 @@ async function bootstrap() {
|
||||
scene.add(dirLight.target);
|
||||
}
|
||||
|
||||
runtime.lui = createWorldLuiRuntime(THREE, runtime);
|
||||
|
||||
const nodeMap = new Map();
|
||||
const pendingModelLoads = [];
|
||||
const gltfLoader = new GLTFLoader();
|
||||
@ -3594,7 +4117,7 @@ async function bootstrap() {
|
||||
emitRuntimeEvent(runtime, "ready", readyInfo);
|
||||
|
||||
setStatus(
|
||||
`Scene ready. Nodes: ${sceneNodes.length}, Animations: ${runtime.animationControllers.length}, Scripts: ${scriptStates.length}${runtime.unsupportedScriptCount > 0 ? ` (unsupported: ${runtime.unsupportedScriptCount})` : ""}, Skybox: ${skyboxState.enabled ? "on" : "off"}, ToneMap: ${renderProfile.toneMappingEnabled ? "on" : "off"}, Shadows: ${renderProfile.shadowEnabled ? "on" : "off"}, Fog: ${renderProfile.fogApplied ? "on" : "off"}, Bloom: ${bloomSetup.enabled ? "on" : "off"}.\nUse mouse to orbit, wheel to zoom.`,
|
||||
`Scene ready. Nodes: ${sceneNodes.length}, LUI3D: ${runtime.lui?.count || 0}, Animations: ${runtime.animationControllers.length}, Scripts: ${scriptStates.length}${runtime.unsupportedScriptCount > 0 ? ` (unsupported: ${runtime.unsupportedScriptCount})` : ""}, Skybox: ${skyboxState.enabled ? "on" : "off"}, ToneMap: ${renderProfile.toneMappingEnabled ? "on" : "off"}, Shadows: ${renderProfile.shadowEnabled ? "on" : "off"}, Fog: ${renderProfile.fogApplied ? "on" : "off"}, Bloom: ${bloomSetup.enabled ? "on" : "off"}.\nUse mouse to orbit, wheel to zoom.`,
|
||||
"ok",
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user