ai改了半天还是不对
This commit is contained in:
+146
-5
@@ -17,7 +17,7 @@ import "react-calendar-timeline/style.css";
|
||||
|
||||
type Plot = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; media: string; schema: string; diagnostics: string; taskflow: string};
|
||||
type Option = {value: string; label: string};
|
||||
type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "partition-grid" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "marker-list" | "surface-marker-list" | "json";
|
||||
type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "partition-grid" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "marker-list" | "surface-marker-list" | "axis3d" | "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[]};
|
||||
@@ -95,10 +95,23 @@ type Frame_Policy_State = {
|
||||
latest_completion_ms: number; average_completion_ms: number; maximum_completion_ms: number};
|
||||
last_frame: {sequence: number; request_source: "unspecified" | "periodic" | "immediate" | "maximum_rate"};
|
||||
};
|
||||
type Datoviz_Frame_Observation = {
|
||||
render_sequence: number;
|
||||
path: "recorded" | "reused";
|
||||
gpu_timing_requested: boolean;
|
||||
readback_requested: boolean;
|
||||
timings_ms: {render_domain_queue_wait: number; apply: number; emit: number; execute: number;
|
||||
submit: number; gpu_fence_wait: number; readback: number};
|
||||
traffic: {uploaded_bytes: number; readback_bytes: number};
|
||||
gpu_ms?: {render: number; transition: number; copy: number; total: number};
|
||||
frame_plan: {resource_version: number; frame_index: number; status: number; artifact_json?: string};
|
||||
validation: {performed: boolean; ok: boolean; code: number; command_index: number};
|
||||
};
|
||||
type Taskflow_Frame_Trace = {sequence: number; correlation_id: number; created_time_unix_ns: number; worker_count: number;
|
||||
request_source?: "unspecified" | "periodic" | "immediate" | "maximum_rate";
|
||||
frame_policy?: Frame_Policy_State;
|
||||
markers?: Record<string, number>; measurements?: Record<string, number>;
|
||||
datoviz?: Datoviz_Frame_Observation;
|
||||
graphs: Taskflow_Graph_Trace[]; executions: Taskflow_Execution_Trace[]};
|
||||
type Taskflow_Frame_Response = {protocol: "aethera.taskflow.frames"; version: 1; requested: number; remaining: number;
|
||||
captured: number; complete: boolean; frames: Taskflow_Frame_Trace[]; media_requested?: number;
|
||||
@@ -510,6 +523,8 @@ function use_gallery_videos(plots: Plot[], transport_mode: Gallery_Transport_Mod
|
||||
? await event.data.arrayBuffer() : null;
|
||||
if (!buffer || stopped || !pixel_surface) return;
|
||||
const sequence = pixel_surface.render(buffer);
|
||||
if (socket.readyState === WebSocket.OPEN)
|
||||
socket.send(JSON.stringify({kind: "pixel_frame_consumed", sequence}));
|
||||
if (!pixel_live) {
|
||||
pixel_live = true;
|
||||
update(current => ({...current, status: "LIVE",
|
||||
@@ -929,6 +944,87 @@ function Structured_Control({field, on_change}: {field: Field; on_change: (value
|
||||
</div></fieldset>;
|
||||
}
|
||||
|
||||
type Axis_3D_Value = {
|
||||
range: {origin: number; target: number};
|
||||
scale: "linear" | "logarithmic" | "time";
|
||||
label: string;
|
||||
unit: string;
|
||||
target_tick_count: number;
|
||||
precision: number;
|
||||
visible: boolean;
|
||||
grid_visible: boolean;
|
||||
labels_visible: boolean;
|
||||
};
|
||||
|
||||
function axis_3d_value(value: unknown): Axis_3D_Value {
|
||||
const source = value && typeof value === "object" ? value as Partial<Axis_3D_Value> : {};
|
||||
const range = source.range && typeof source.range === "object" ? source.range : {origin: 0, target: 1};
|
||||
return {
|
||||
range: {origin: Number(range.origin ?? 0), target: Number(range.target ?? 1)},
|
||||
scale: source.scale === "logarithmic" || source.scale === "time" ? source.scale : "linear",
|
||||
label: String(source.label ?? ""), unit: String(source.unit ?? ""),
|
||||
target_tick_count: Number(source.target_tick_count ?? 6), precision: Number(source.precision ?? 2),
|
||||
visible: source.visible ?? true, grid_visible: source.grid_visible ?? true,
|
||||
labels_visible: source.labels_visible ?? true
|
||||
};
|
||||
}
|
||||
|
||||
function axis_3d_error(value: unknown) {
|
||||
const axis = axis_3d_value(value);
|
||||
if (!Number.isFinite(axis.range.origin) || !Number.isFinite(axis.range.target)) return "轴范围必须是有限数字";
|
||||
if (axis.range.origin === axis.range.target) return "轴范围起点和终点不能相同";
|
||||
if (axis.scale === "logarithmic" && (axis.range.origin <= 0 || axis.range.target <= 0))
|
||||
return "对数频率轴的起点和终点必须大于 0";
|
||||
if (!Number.isInteger(axis.target_tick_count) || axis.target_tick_count < 2 || axis.target_tick_count > 64)
|
||||
return "目标刻度数必须是 2 到 64 的整数";
|
||||
if (!Number.isInteger(axis.precision) || axis.precision < 0 || axis.precision > 12)
|
||||
return "小数精度必须是 0 到 12 的整数";
|
||||
return "";
|
||||
}
|
||||
|
||||
function Axis_3D_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) {
|
||||
const axis = axis_3d_value(field.value);
|
||||
const is_frequency = axis.unit.trim().toLowerCase() === "hz" || axis.label.trim().toLowerCase() === "frequency";
|
||||
const semantic = axis.scale === "time" ? "time" : is_frequency ? "frequency" : "numeric";
|
||||
const semantic_label = semantic === "time" ? "时间轴" : semantic === "frequency" ? "频率轴" : "数值轴";
|
||||
const update = (next: Partial<Axis_3D_Value>) => on_change({...axis, ...next});
|
||||
const update_range = (next: Partial<Axis_3D_Value["range"]>) => update({range: {...axis.range, ...next}});
|
||||
const select_semantic = (next: string) => {
|
||||
if (next === "time") update({scale: "time", label: "Time", unit: "s"});
|
||||
else if (next === "frequency") update({scale: "logarithmic", label: "Frequency", unit: "Hz",
|
||||
range: axis.range.origin > 0 && axis.range.target > 0 ? axis.range : {origin: 10, target: 20_000}});
|
||||
else update({scale: axis.scale === "time" ? "linear" : axis.scale});
|
||||
};
|
||||
const error = axis_3d_error(axis);
|
||||
return <fieldset className="control structuredControl axis3dControl" title={field_tooltip(field)}>
|
||||
<legend>{field_label(field)} · {semantic_label} <code>{field.key}</code></legend>
|
||||
<div className="axis3dGrid">
|
||||
<label><span>轴语义</span><select value={semantic} onChange={event => select_semantic(event.target.value)}>
|
||||
<option value="numeric">数值轴</option><option value="time">时间轴</option><option value="frequency">频率轴</option>
|
||||
</select></label>
|
||||
<label><span>坐标变换</span><select value={axis.scale} onChange={event => update({scale: event.target.value as Axis_3D_Value["scale"]})}>
|
||||
<option value="linear">线性</option><option value="logarithmic">对数</option><option value="time">时间</option>
|
||||
</select></label>
|
||||
<label><span>{semantic === "time" ? "起始时间" : semantic === "frequency" ? "最低频率" : "范围起点"}</span>
|
||||
<input type="number" step="any" value={axis.range.origin} onChange={event => update_range({origin: Number(event.target.value)})}/></label>
|
||||
<label><span>{semantic === "time" ? "结束时间" : semantic === "frequency" ? "最高频率" : "范围终点"}</span>
|
||||
<input type="number" step="any" value={axis.range.target} onChange={event => update_range({target: Number(event.target.value)})}/></label>
|
||||
<label><span>轴标题</span><input value={axis.label} onChange={event => update({label: event.target.value})}/></label>
|
||||
<label><span>单位</span><input value={axis.unit} onChange={event => update({unit: event.target.value})}/></label>
|
||||
<label><span>目标刻度数</span><input type="number" min="2" max="64" step="1" value={axis.target_tick_count}
|
||||
onChange={event => update({target_tick_count: Number(event.target.value)})}/></label>
|
||||
<label><span>小数精度</span><input type="number" min="0" max="12" step="1" value={axis.precision}
|
||||
onChange={event => update({precision: Number(event.target.value)})}/></label>
|
||||
</div>
|
||||
<div className="axis3dToggles">
|
||||
<label><input type="checkbox" checked={axis.visible} onChange={event => update({visible: event.target.checked})}/>显示轴</label>
|
||||
<label><input type="checkbox" checked={axis.grid_visible} onChange={event => update({grid_visible: event.target.checked})}/>显示网格</label>
|
||||
<label><input type="checkbox" checked={axis.labels_visible} onChange={event => update({labels_visible: event.target.checked})}/>显示刻度文字</label>
|
||||
</div>
|
||||
{error ? <small className="error">{error}</small> : <small>范围和单位直接写入当前 Axes_3D 属性;渲染帧使用同一份描述生成 Datoviz 轴线、网格和文字。</small>}
|
||||
</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; coordinate_label_visible: boolean};
|
||||
const marker_shapes = ["disc", "square", "triangle", "diamond", "cross"];
|
||||
|
||||
@@ -954,6 +1050,7 @@ function Field_Control({field, on_change}: {field: Field; on_change: (value: unk
|
||||
if (["color", "point2", "size", "rect", "range", "partition-grid", "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 (field.editor === "axis3d") return <Axis_3D_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>;
|
||||
@@ -968,6 +1065,7 @@ function Property_Control({field, on_commit}: {field: Field; on_commit: (value:
|
||||
const [dirty, set_dirty] = useState(false);
|
||||
const [saving, set_saving] = useState(false);
|
||||
const encoded_value = JSON.stringify(field.value);
|
||||
const validation_error = field.editor === "axis3d" ? axis_3d_error(draft) : "";
|
||||
useEffect(() => { set_draft(field.value); set_dirty(false); }, [encoded_value]);
|
||||
const change = (value: unknown) => {
|
||||
set_draft(value);
|
||||
@@ -984,7 +1082,7 @@ function Property_Control({field, on_commit}: {field: Field; on_commit: (value:
|
||||
return <div className={`propertyEditor${dirty ? " dirty" : ""}`}>
|
||||
<Field_Control field={{...field, value: draft}} on_change={change}/>
|
||||
{field.editor !== "boolean" ? <div className="propertyActions">
|
||||
<button disabled={!dirty || saving} onClick={() => void commit()}>{saving ? "提交中…" : "确定"}</button>
|
||||
<button disabled={!dirty || saving || Boolean(validation_error)} onClick={() => void commit()}>{saving ? "提交中…" : "确定"}</button>
|
||||
<button disabled={!dirty || saving} onClick={() => { set_draft(field.value); set_dirty(false); }}>撤销</button>
|
||||
</div> : saving ? <small className="savingHint">正在提交…</small> : null}
|
||||
</div>;
|
||||
@@ -1237,6 +1335,9 @@ function taskflow_node_name(name: string, node?: Taskflow_Node_Trace) {
|
||||
if (name === "scene.paint") return "Scene · 绘制子图";
|
||||
if (name === "scene.paint.complete") return "Scene · 像素绘制完成";
|
||||
if (name === "scene.completion") return "Scene · 帧完成处理";
|
||||
if (name === "scene.backend.submit") return "3D Scene · 准备并提交 Datoviz 帧";
|
||||
if (name === "render_3d.backend.collect") return "Datoviz · GPU 完成后读回";
|
||||
if (name === "render_3d.frame.retire") return "3D Scene · 完成帧退役";
|
||||
if (name === "plot.frame.publish") return "Plot · 发布完成帧";
|
||||
if (name === "gallery.sample.capture") return "Gallery · 30 FPS 最近帧采样";
|
||||
if (name === "FFmpeg.H264.encode" || name === "gallery.h264.encode") return "FFmpeg H.264 · 编码";
|
||||
@@ -1678,7 +1779,7 @@ function taskflow_render_domain_state(node: Taskflow_Node_Trace, frame?: Taskflo
|
||||
markers[first] === undefined || markers[last] === undefined
|
||||
? null : Math.max(0, markers[last] - markers[first]);
|
||||
const prepare = interval("backend_prepare_started", "backend_prepare_finished");
|
||||
const submit_queue = interval("backend_submit_queued", "backend_queue_left");
|
||||
const submit_queue = interval("backend_queue_entered", "backend_queue_left");
|
||||
const gpu = interval("gpu_submitted", "gpu_completed");
|
||||
return {
|
||||
invoke_result: markers.backend_prepare_finished === undefined
|
||||
@@ -1687,7 +1788,8 @@ function taskflow_render_domain_state(node: Taskflow_Node_Trace, frame?: Taskflo
|
||||
submit_queue_ms: submit_queue,
|
||||
gpu_completion_ms: gpu,
|
||||
frame_sequence: frame.sequence,
|
||||
correlation_id: frame.correlation_id
|
||||
correlation_id: frame.correlation_id,
|
||||
datoviz: frame.datoviz ?? null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1886,6 +1988,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
|
||||
frame_policy: frame?.frame_policy,
|
||||
markers: frame?.markers ?? {},
|
||||
measurements: frame?.measurements ?? {},
|
||||
datoviz: frame?.datoviz,
|
||||
graph,
|
||||
expanded_graph: expanded_taskflow_graph(graph),
|
||||
/* 保留当前选中拓扑,同时把本帧其余真实 Task_Graph 一并复制。
|
||||
@@ -2062,7 +2165,11 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state,
|
||||
`${label} ${milliseconds(end - start)}`, label, `${start_key} → ${end_key}`);
|
||||
});
|
||||
|
||||
add_lifecycle_group("lifecycle-backend", "后端与 GPU", "准备 → 队列 → GPU → 回读");
|
||||
const datoviz = frame.datoviz;
|
||||
const datoviz_detail = datoviz
|
||||
? `${datoviz.path} · 上传 ${datoviz.traffic.uploaded_bytes.toLocaleString("zh-CN")} B · 读回 ${datoviz.traffic.readback_bytes.toLocaleString("zh-CN")} B`
|
||||
: "准备 → 队列 → GPU → 回读";
|
||||
add_lifecycle_group("lifecycle-backend", "后端与 GPU", datoviz_detail);
|
||||
const backend_intervals: Array<[string, string, string, string]> = [
|
||||
["backend_prepare_started", "backend_prepare_finished", "后端准备", "backend-prepare"],
|
||||
["backend_queue_entered", "backend_queue_left", "提交队列", "backend-queue"],
|
||||
@@ -2161,6 +2268,7 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state,
|
||||
frame_policy: frame?.frame_policy,
|
||||
markers: frame?.markers ?? {},
|
||||
measurements: frame?.measurements ?? {},
|
||||
datoviz: frame?.datoviz,
|
||||
graph,
|
||||
timeline: {origin: "render_frame_created", submitted_ms: graph.submitted_ms, finished_ms: graph.finished_ms,
|
||||
wall_time_ms: Math.max(0, graph.finished_ms - graph.submitted_ms), last_execution_completed_ms: model.last_completed,
|
||||
@@ -2255,6 +2363,38 @@ function Taskflow_Timeline({graph, executions, frame, components, gallery_state,
|
||||
</section>;
|
||||
}
|
||||
|
||||
function frame_bytes(value: number) {
|
||||
if (!Number.isFinite(value)) return "--";
|
||||
if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MiB`;
|
||||
if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
|
||||
return `${value.toLocaleString("zh-CN")} B`;
|
||||
}
|
||||
|
||||
function Datoviz_Frame_Summary({observation}: {observation: Datoviz_Frame_Observation}) {
|
||||
const timing = observation.timings_ms;
|
||||
const gpu = observation.gpu_ms;
|
||||
const validation = observation.validation;
|
||||
const structural = observation.path === "recorded";
|
||||
return <section className="datovizFrameObservation" aria-label="本帧 Datoviz 观测">
|
||||
<header><div><strong>本帧 Datoviz 路径</strong><span>观测数据归属于 Frame_3D #{observation.render_sequence},不读取独立历史状态。</span></div>
|
||||
<i className={structural ? "recorded" : "reused"}>{structural ? "重新录制命令" : "复用录制命令"}</i></header>
|
||||
<dl className="taskflowGraphSummary">
|
||||
<div title="帧进入 GPU Render Domain 单写者队列到开始执行的墙钟等待。"><dt>Render Domain 排队</dt><dd>{milliseconds(timing.render_domain_queue_wait)}</dd></div>
|
||||
<div title="属性应用;复用帧只包含当前目标槽的映射缓冲更新。"><dt>Datoviz Apply</dt><dd>{milliseconds(timing.apply)}</dd></div>
|
||||
<div title="生成 Frame Plan;复用命令路径应为 0。"><dt>Frame Plan emit</dt><dd>{milliseconds(timing.emit)}</dd></div>
|
||||
<div title="执行 DRP2 Frame Plan;复用命令路径应为 0。"><dt>DRP2 execute</dt><dd>{milliseconds(timing.execute)}</dd></div>
|
||||
<div><dt>vkQueueSubmit</dt><dd>{milliseconds(timing.submit)}</dd></div>
|
||||
<div title={gpu ? `渲染 ${milliseconds(gpu.render)} · 转换 ${milliseconds(gpu.transition)} · 拷贝 ${milliseconds(gpu.copy)}` : "该帧没有请求 GPU timestamp"}><dt>GPU 总耗时</dt><dd>{gpu ? milliseconds(gpu.total) : "未采集"}</dd></div>
|
||||
<div title="Gpu_Completion_Service 异步等待 fence 的实际墙钟。"><dt>GPU fence 等待</dt><dd>{milliseconds(timing.gpu_fence_wait)}</dd></div>
|
||||
<div title="fence 完成后在 Taskflow worker 执行的目标槽读回。"><dt>像素读回</dt><dd>{milliseconds(timing.readback)}</dd></div>
|
||||
<div><dt>动态数据上传</dt><dd>{frame_bytes(observation.traffic.uploaded_bytes)}</dd></div>
|
||||
<div><dt>帧读回流量</dt><dd>{frame_bytes(observation.traffic.readback_bytes)}</dd></div>
|
||||
<div><dt>Frame Plan</dt><dd>v{observation.frame_plan.resource_version} · 槽 {observation.frame_plan.frame_index}</dd></div>
|
||||
<div><dt>验证</dt><dd>{validation.performed ? validation.ok ? "通过" : `失败 ${validation.code}` : "未执行"}</dd></div>
|
||||
</dl>
|
||||
</section>;
|
||||
}
|
||||
|
||||
function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Component[]}) {
|
||||
const [frame_count, set_frame_count] = useState(8);
|
||||
const [scene_response, set_scene_response] = useState<Taskflow_Frame_Response | null>(null);
|
||||
@@ -2365,6 +2505,7 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
|
||||
<button className={view_mode === "timeline" ? "active" : ""} onClick={() => set_view_mode("timeline")}>单帧执行时间轴</button>
|
||||
</div>
|
||||
<span>Executor {frame.worker_count} workers · 本帧 {frame.executions.length} 次任务执行</span></section>
|
||||
{frame.datoviz ? <Datoviz_Frame_Summary observation={frame.datoviz}/> : null}
|
||||
{displayed_view_mode === "aggregate" && aggregate ? <><dl className="taskflowGraphSummary taskflowAggregateSummary">
|
||||
<div><dt>业务阶段</dt><dd>{aggregate.graph.stage}</dd></div>
|
||||
<div><dt>捕获帧 / 阶段样本</dt><dd>{aggregate.frames} / {aggregate.samples}</dd></div>
|
||||
|
||||
@@ -220,6 +220,14 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
|
||||
.taskflowGraphSummary > div, .taskflowRuntimeSummary > div { min-width: 0; padding: 10px; border: 1px solid #213653; border-radius: 9px; background: #0a1422; }
|
||||
.taskflowGraphSummary dt, .taskflowRuntimeSummary dt { color: #71839e; font-size: 10px; }
|
||||
.taskflowGraphSummary dd, .taskflowRuntimeSummary dd { margin: 5px 0 0; color: #5ce4c2; font: 700 13px/1.35 ui-monospace, monospace; white-space: normal; overflow-wrap: anywhere; }
|
||||
.datovizFrameObservation { overflow: hidden; border: 1px solid #28516a; border-radius: 10px; background: #081522; }
|
||||
.datovizFrameObservation > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid #203c53; background: #0d1d2c; }
|
||||
.datovizFrameObservation > header > div { display: grid; gap: 3px; }
|
||||
.datovizFrameObservation > header strong { color: #dce8f8; font-size: 11px; }
|
||||
.datovizFrameObservation > header span { color: #7189a8; font-size: 9px; }
|
||||
.datovizFrameObservation > header i { flex: none; padding: 5px 8px; border: 1px solid #337360; border-radius: 99px; color: #72e3c8; background: #0b2822; font: 700 9px/1 ui-monospace, monospace; font-style: normal; }
|
||||
.datovizFrameObservation > header i.recorded { color: #efc073; border-color: #76572d; background: #291d0d; }
|
||||
.datovizFrameObservation .taskflowGraphSummary { padding: 10px; }
|
||||
.taskflowDagSection { min-width: 640px; overflow: visible; border: 1px solid #213653; border-radius: 10px; background: #07101c; }
|
||||
.taskflowDagToolbar { display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap; gap: 12px; padding: 9px 11px; border-bottom: 1px solid #213653; background: #0c1727; }
|
||||
.taskflowDagToolbar button { flex: none; padding: 6px 9px; color: #b9cce3; border: 1px solid #36516f; border-radius: 6px; background: #132238; cursor: pointer; }
|
||||
@@ -337,6 +345,13 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
|
||||
.componentCard > .propGrid { padding: 13px; }
|
||||
.structuredControl { min-width: 0; margin: 0; padding: 10px; border: 1px solid #263b59; border-radius: 9px; }
|
||||
.structuredControl legend { padding: 0 5px; color: #c8d5e8; }
|
||||
.axis3dControl { gap: 10px; background: #0a1524; }
|
||||
.axis3dGrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.axis3dGrid label { display: grid; min-width: 0; gap: 5px; color: #8298b4; font-size: 10px; }
|
||||
.axis3dToggles { display: flex; flex-wrap: wrap; gap: 8px 13px; color: #9eb1c9; font-size: 10px; }
|
||||
.axis3dToggles label { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.axis3dToggles input[type="checkbox"] { width: 17px; height: 17px; }
|
||||
@media (max-width: 620px) { .axis3dGrid { grid-template-columns: 1fr; } }
|
||||
.inlineFields { display: grid; grid-template-columns: repeat(auto-fit, minmax(82px, 1fr)); gap: 8px; }
|
||||
.inlineFields > label { display: grid; gap: 4px; min-width: 0; color: #8195b1; font-size: 11px; }
|
||||
.inlineFields input[type="color"] { min-height: 40px; padding: 3px; }
|
||||
|
||||
Reference in New Issue
Block a user