三维频谱marker
This commit is contained in:
+48
-11
@@ -15,7 +15,7 @@ echarts.use([LineChart, DataZoomComponent, GridComponent, LegendComponent, Toolt
|
||||
|
||||
type Plot = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; schema: string};
|
||||
type Option = {value: string; label: string};
|
||||
type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "json";
|
||||
type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "marker-list" | "surface-marker-list" | "json";
|
||||
type Color_Channel_Scale = "normalized" | "byte";
|
||||
type Field = {key: string; label: string; description: string; technical_description?: string; editor: Editor; editable: boolean; value: unknown; options?: Option[]; color_channel_scale?: Color_Channel_Scale; minimum?: number; maximum?: number; step?: number};
|
||||
type Component = {id: string; label: string; kind: string; fields: Field[]; state: Record<string, unknown>};
|
||||
@@ -443,11 +443,9 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
|
||||
const detail = (event as CustomEvent<{plot_id: string}>).detail;
|
||||
const canvas = canvas_ref.current;
|
||||
if (detail?.plot_id !== plot.id || !canvas) return;
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
const position = {x: bounds.width * devicePixelRatio * 0.5, y: bounds.height * devicePixelRatio * 0.5};
|
||||
const global_position = {x: (bounds.left + bounds.width * 0.5) * devicePixelRatio, y: (bounds.top + bounds.height * 0.5) * devicePixelRatio};
|
||||
const pointer = (type: "pointer_press" | "pointer_release", buttons: number) => transmit("input", {type, position, global_position, button: "left", buttons, modifiers: 0});
|
||||
pointer("pointer_press", 1); pointer("pointer_release", 0); pointer("pointer_press", 1); pointer("pointer_release", 0);
|
||||
canvas.focus({preventScroll: true});
|
||||
transmit("input", {type: "key_press", key: "home", native_key: 36, modifiers: 0, auto_repeat: false});
|
||||
transmit("input", {type: "key_release", key: "home", native_key: 36, modifiers: 0, auto_repeat: false});
|
||||
};
|
||||
const on_diagnostics_reset = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{plot_id: string}>).detail;
|
||||
@@ -657,9 +655,30 @@ function Structured_Control({field, on_change}: {field: Field; on_change: (value
|
||||
</div></fieldset>;
|
||||
}
|
||||
|
||||
type Marker_Value = {position: {x: number; y: number; z: number}; color: {red: number; green: number; blue: number; alpha: number}; diameter_px: number; angle: number; shape: string};
|
||||
const marker_shapes = ["disc", "square", "triangle", "diamond", "cross"];
|
||||
|
||||
function Marker_List_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) {
|
||||
const markers = Array.isArray(field.value) ? field.value as Marker_Value[] : [];
|
||||
const surface_attached = field.editor === "surface-marker-list";
|
||||
const replace = (index: number, marker: Marker_Value) => on_change(markers.map((value, item_index) => item_index === index ? marker : value));
|
||||
const add = () => on_change([...markers, {position: {x: 0, y: 0, z: 0}, color: {red: 255, green: 218, blue: 112, alpha: 255}, diameter_px: 22, angle: 0, shape: "diamond"}]);
|
||||
return <fieldset className="control markerListControl" title={field_tooltip(field)}><legend>{field_label(field)} <code>{field.key}</code></legend>
|
||||
<div className="markerToolbar"><span>{markers.length} 个 Marker</span><button type="button" onClick={add}>添加 Marker</button></div>
|
||||
<div className="markerRows">{markers.map((marker, index) => <section className="markerRow" key={index}>
|
||||
<header><strong>Marker {index + 1}</strong><button type="button" onClick={() => on_change(markers.filter((_, item_index) => item_index !== index))}>删除</button></header>
|
||||
<div className="markerCoordinates">{(["x", "y", "z"] as const).map(axis => <label key={axis}><span>{axis === "z" && surface_attached ? "Z(谱面自动值)" : axis.toUpperCase()}</span><input type="number" min="-1" max="1" step="0.01" disabled={axis === "z" && surface_attached} value={marker.position?.[axis] ?? 0} onChange={event => replace(index, {...marker, position: {...marker.position, [axis]: Number(event.target.value)}})}/></label>)}</div>
|
||||
<div className="markerAppearance"><label><span>形状</span><select value={marker.shape ?? "diamond"} onChange={event => replace(index, {...marker, shape: event.target.value})}>{marker_shapes.map(shape => <option key={shape} value={shape}>{enum_label(shape)}</option>)}</select></label>
|
||||
<label><span>直径 px</span><input type="number" min="1" max="256" step="1" value={marker.diameter_px ?? 22} onChange={event => replace(index, {...marker, diameter_px: Number(event.target.value)})}/></label>
|
||||
<label><span>颜色</span><input type="color" value={rgba_hex(marker.color ?? {red: 255, green: 218, blue: 112, alpha: 255})} onChange={event => replace(index, {...marker, color: update_rgb(marker.color, event.target.value) as Marker_Value["color"]})}/></label></div>
|
||||
</section>)}</div>
|
||||
</fieldset>;
|
||||
}
|
||||
|
||||
function Field_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) {
|
||||
if (["color", "point2", "size", "rect", "range", "pen", "brush", "font", "vector3"].includes(field.editor))
|
||||
return <Structured_Control field={field} on_change={on_change}/>;
|
||||
if (field.editor === "marker-list" || field.editor === "surface-marker-list") return <Marker_List_Control field={field} on_change={on_change}/>;
|
||||
if (["json", "color-map", "matrix4"].includes(field.editor)) return <Json_Control field={field} on_change={on_change}/>;
|
||||
if (field.editor === "boolean") return <label className="control controlBoolean" title={field_tooltip(field)}><span>{field_label(field)} <code>{field.key}</code></span>
|
||||
<input type="checkbox" checked={Boolean(field.value)} onChange={event => on_change(event.target.checked)}/></label>;
|
||||
@@ -925,6 +944,22 @@ function State_Component({component, histories}: {component: Component; historie
|
||||
<details className="rawState"><summary>原始状态 JSON</summary><pre className="stateJson">{JSON.stringify(component.state, null, 2)}</pre></details></section>;
|
||||
}
|
||||
|
||||
const camera_mode_descriptions: Record<string, {label: string; usage: string}> = {
|
||||
turntable: {label: "Turntable 环绕", usage: "左键环绕目标,右键或中键平移,滚轮改变观察距离。"},
|
||||
arcball: {label: "Arcball 自由旋转", usage: "拖动虚拟轨迹球进行无固定水平面的自由旋转,滚轮缩放。"},
|
||||
fly: {label: "Fly 第一人称", usage: "拖动改变观察方向,W/A/S/D 和方向键移动,Shift 加速,滚轮前后推进。"},
|
||||
panzoom: {label: "Panzoom 平面导航", usage: "拖动二维平面,滚轮缩放;适合固定观察方向下检查截面。"}
|
||||
};
|
||||
|
||||
function Camera_Mode_Guide({fields}: {fields: Field[]}) {
|
||||
const active = String(fields.find(field => field.key === "controller")?.value ?? "turntable");
|
||||
return <div className="cameraModeGuide" aria-label="Datoviz 原生相机模式">
|
||||
{Object.entries(camera_mode_descriptions).map(([key, mode]) => <div key={key} className={key === active ? "active" : ""}>
|
||||
<strong>{mode.label}</strong><span>{mode.usage}</span>
|
||||
</div>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Property_Pane({plot, schema, busy, on_refresh, on_update}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; on_update: (component: Component, field: Field, value: unknown) => Promise<void>}) {
|
||||
const {components, selected, set_active} = use_active_component(schema);
|
||||
const component = components.find(item => item.id === selected);
|
||||
@@ -933,7 +968,9 @@ function Property_Pane({plot, schema, busy, on_refresh, on_update}: {plot: Plot;
|
||||
return <section className="workspacePane"><Workspace_Header plot={plot} label="属性编辑" count={count} busy={busy} on_refresh={on_refresh}/>
|
||||
<Component_Tabs components={components} selected={selected} on_select={set_active}/><div className="workspaceBody">
|
||||
{busy && !schema ? <p className="muted">正在读取组件信息…</p> : component ? <section className="componentContent"><div className="sectionIntro">
|
||||
<strong>{component.label}</strong><span>{fields.length} 个可编辑属性</span></div>
|
||||
<strong>{component.label}</strong><span>{fields.length} 个可编辑属性</span>
|
||||
{component.kind === "camera" ? <button className="componentAction" onClick={() => window.dispatchEvent(new CustomEvent("aethera-reset-camera", {detail: {plot_id: plot.id}}))}>复位到初始视角</button> : null}</div>
|
||||
{component.kind === "camera" ? <Camera_Mode_Guide fields={fields}/> : null}
|
||||
<div className="propGrid">
|
||||
{fields.map(field => <Property_Control key={field.key} field={field} on_commit={value => on_update(component, field, value)}/>)}</div></section> : null}</div></section>;
|
||||
}
|
||||
@@ -973,14 +1010,14 @@ function Data_Generator_View({plot, generator, on_generated}: {plot: Plot; gener
|
||||
{status ? <p className="analysisStatus">{status}</p> : null}</section>;
|
||||
}
|
||||
|
||||
function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manual_frame, on_camera_reset, on_reset}: {
|
||||
function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manual_frame, on_reset}: {
|
||||
plot: Plot; analysis: Frame_Analysis | null; busy: boolean; on_refresh: () => void; on_update: (component: Frame_Analysis, field: Field, value: unknown) => Promise<void>;
|
||||
on_manual_frame: () => void; on_camera_reset: () => void; on_reset: () => void;
|
||||
on_manual_frame: () => void; on_reset: () => void;
|
||||
}) {
|
||||
const fields = analysis?.fields.filter(field => field.editable) ?? [];
|
||||
return <section className="workspacePane"><Workspace_Header plot={plot} label="采样与帧策略" count={fields.length} busy={busy} on_refresh={on_refresh}/>
|
||||
<div className="workspaceBody"><section className="analysisSection"><header><div><strong>{plot.dimension} 持续采样控制</strong><span>选中或可见时传输完整像素;离屏时继续真实渲染并只回传轻量诊断。2D 与 3D 使用各自的采样频率和阶段模型。</span></div>
|
||||
<div className="framePolicyActions"><button onClick={on_manual_frame}>手动生成一帧</button>{plot.dimension === "3D" ? <button onClick={on_camera_reset}>复位相机</button> : null}<button onClick={on_reset}>清空并从头统计</button></div></header>
|
||||
<div className="framePolicyActions"><button onClick={on_manual_frame}>手动生成一帧</button><button onClick={on_reset}>清空并从头统计</button></div></header>
|
||||
{analysis ? <div className="propGrid">{fields.map(field => <Property_Control key={field.key} field={field} on_commit={value => on_update(analysis, field, value)}/>)}</div> : <p className="muted">正在读取帧策略…</p>}
|
||||
</section></div></section>;
|
||||
}
|
||||
@@ -1216,7 +1253,7 @@ export function App() {
|
||||
if (node.getComponent() === "state") return <aside className="inspector" aria-label="状态查看面板"><State_Pane plot={selected} schema={schema} busy={schema_busy} on_refresh={() => void load_schema(true)} histories={state_histories}/></aside>;
|
||||
if (node.getComponent() === "frame-policy") return <aside className="inspector" aria-label="采样与帧策略"><Frame_Policy_Pane plot={selected} analysis={schema?.frame_analysis ?? null} busy={schema_busy} on_refresh={() => void load_schema(true)} on_update={update}
|
||||
on_manual_frame={() => window.dispatchEvent(new CustomEvent("aethera-manual-frame", {detail: {plot_id: selected.id}}))}
|
||||
on_camera_reset={() => window.dispatchEvent(new CustomEvent("aethera-reset-camera", {detail: {plot_id: selected.id}}))} on_reset={reset_frame_diagnostics}/></aside>;
|
||||
on_reset={reset_frame_diagnostics}/></aside>;
|
||||
if (node.getComponent() === "data-generation") return <aside className="inspector" aria-label="原始数据生成"><Data_Generation_Pane plot={selected} analysis={schema?.frame_analysis ?? null} busy={schema_busy} on_refresh={() => void load_schema(true)} on_generated={() => { reset_frame_diagnostics(); window.dispatchEvent(new CustomEvent("aethera-manual-frame", {detail: {plot_id: selected.id}})); }}/></aside>;
|
||||
if (node.getComponent() === "frame-statistics") return <aside className="inspector" aria-label="帧流水线统计"><Frame_Statistics_Pane plot={selected} diagnostics={frame_diagnostics} busy={schema_busy} on_refresh={() => void load_schema(true)} on_reset={reset_frame_diagnostics}/></aside>;
|
||||
return <div className="emptyPane">未知工作区面板。</div>;
|
||||
|
||||
@@ -109,6 +109,21 @@ canvas:focus { outline: 1px solid #5ce4c2; outline-offset: -1px; }
|
||||
.propertyActions button { padding: 6px 10px; color: #b9c9dd; border: 1px solid #304664; border-radius: 6px; background: #101c2d; cursor: pointer; }
|
||||
.propertyActions button:first-child:not(:disabled) { color: #062019; border-color: #5ce4c2; background: #5ce4c2; }
|
||||
.propertyActions button:disabled { opacity: .4; cursor: default; }
|
||||
.componentAction { margin-left: auto; padding: 6px 10px; color: #062019; border: 1px solid #5ce4c2; border-radius: 6px; background: #5ce4c2; cursor: pointer; }
|
||||
.cameraModeGuide { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 0 0 14px; }
|
||||
.cameraModeGuide > div { display: grid; gap: 4px; padding: 9px 10px; border: 1px solid #243850; border-radius: 7px; background: #0a1422; color: #7890ae; }
|
||||
.cameraModeGuide > div.active { border-color: #52dfbd; background: #0c211f; box-shadow: inset 3px 0 #52dfbd; }
|
||||
.cameraModeGuide strong { color: #dce8f8; font-size: 12px; }
|
||||
.cameraModeGuide span { font-size: 11px; line-height: 1.45; }
|
||||
.markerListControl { grid-column: 1 / -1; min-width: 0; }
|
||||
.markerToolbar, .markerRow header, .markerAppearance, .markerCoordinates { display: flex; align-items: center; gap: 8px; }
|
||||
.markerToolbar, .markerRow header { justify-content: space-between; }
|
||||
.markerToolbar button, .markerRow button { padding: 5px 9px; color: #b9c9dd; border: 1px solid #304664; border-radius: 6px; background: #101c2d; cursor: pointer; }
|
||||
.markerRows { display: grid; gap: 9px; margin-top: 10px; }
|
||||
.markerRow { padding: 10px; border: 1px solid #263d59; border-radius: 8px; background: #0b1625; }
|
||||
.markerCoordinates, .markerAppearance { flex-wrap: wrap; margin-top: 8px; }
|
||||
.markerCoordinates label, .markerAppearance label { display: grid; grid-template-columns: auto minmax(64px, 1fr); align-items: center; gap: 6px; flex: 1 1 110px; }
|
||||
.markerCoordinates input, .markerAppearance input, .markerAppearance select { min-width: 0; width: 100%; }
|
||||
.savingHint { display: block; margin-top: 5px; color: #5ce4c2; text-align: right; }
|
||||
.framePolicyActions { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; padding: 11px; border: 1px solid #27594f; border-radius: 9px; background: #0b1b1a; }
|
||||
.framePolicyActions button { flex: 0 0 auto; padding: 8px 12px; color: #062019; border: 1px solid #5ce4c2; border-radius: 7px; background: #5ce4c2; cursor: pointer; }
|
||||
|
||||
Reference in New Issue
Block a user