feat(topology): seed diagram presentation styles
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
|||||||
|
-- topology_api/migrations/006_presentation_styles.sql
|
||||||
|
-- Database-driven presentation semantics for topology diagrams.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS topology.diagram_themes (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
base_id uuid NOT NULL REFERENCES topology.bases(id) ON DELETE CASCADE,
|
||||||
|
theme_key text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
tokens jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
is_default boolean NOT NULL DEFAULT false,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT uq_diagram_themes_base_key UNIQUE (base_id, theme_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_diagram_themes_default_per_base
|
||||||
|
ON topology.diagram_themes(base_id)
|
||||||
|
WHERE is_default;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS topology.diagram_node_styles (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
theme_id uuid NOT NULL REFERENCES topology.diagram_themes(id) ON DELETE CASCADE,
|
||||||
|
style_key text NOT NULL,
|
||||||
|
display_name text NOT NULL,
|
||||||
|
fill_color text NOT NULL,
|
||||||
|
stroke_color text NOT NULL,
|
||||||
|
stroke_width numeric NOT NULL,
|
||||||
|
corner_radius numeric NOT NULL DEFAULT 16,
|
||||||
|
title_color text,
|
||||||
|
meta_color text,
|
||||||
|
hint_color text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT uq_diagram_node_styles_theme_key UNIQUE (theme_id, style_key),
|
||||||
|
CONSTRAINT ck_diagram_node_stroke_width CHECK (stroke_width >= 0),
|
||||||
|
CONSTRAINT ck_diagram_node_corner_radius CHECK (corner_radius >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS topology.diagram_edge_styles (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
theme_id uuid NOT NULL REFERENCES topology.diagram_themes(id) ON DELETE CASCADE,
|
||||||
|
connection_type text NOT NULL,
|
||||||
|
display_name text NOT NULL,
|
||||||
|
stroke_color text NOT NULL,
|
||||||
|
stroke_width numeric NOT NULL,
|
||||||
|
dash_array text,
|
||||||
|
line_cap text NOT NULL DEFAULT 'square',
|
||||||
|
legend_group text,
|
||||||
|
legend_order integer NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT uq_diagram_edge_styles_theme_type UNIQUE (theme_id, connection_type),
|
||||||
|
CONSTRAINT ck_diagram_edge_stroke_width CHECK (stroke_width >= 0),
|
||||||
|
CONSTRAINT ck_diagram_edge_line_cap CHECK (line_cap IN ('butt', 'round', 'square')),
|
||||||
|
CONSTRAINT ck_diagram_edge_dash_array CHECK (
|
||||||
|
dash_array IS NULL OR dash_array ~ '^[0-9]+( [0-9]+)*$'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS topology.diagram_decoration_styles (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
theme_id uuid NOT NULL REFERENCES topology.diagram_themes(id) ON DELETE CASCADE,
|
||||||
|
decoration_key text NOT NULL,
|
||||||
|
display_name text NOT NULL,
|
||||||
|
fill_color text NOT NULL,
|
||||||
|
stroke_color text NOT NULL,
|
||||||
|
stroke_width numeric NOT NULL,
|
||||||
|
label text,
|
||||||
|
label_color text,
|
||||||
|
render_params jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
legend_order integer NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT uq_diagram_decoration_styles_theme_key UNIQUE (theme_id, decoration_key),
|
||||||
|
CONSTRAINT ck_diagram_decoration_stroke_width CHECK (stroke_width >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS topology.diagram_legend_items (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
theme_id uuid NOT NULL REFERENCES topology.diagram_themes(id) ON DELETE CASCADE,
|
||||||
|
legend_key text NOT NULL,
|
||||||
|
label text NOT NULL,
|
||||||
|
item_type text NOT NULL,
|
||||||
|
style_ref text NOT NULL,
|
||||||
|
display_order integer NOT NULL DEFAULT 0,
|
||||||
|
is_visible boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT uq_diagram_legend_items_theme_key UNIQUE (theme_id, legend_key),
|
||||||
|
CONSTRAINT ck_diagram_legend_item_type CHECK (item_type IN ('edge', 'decoration', 'node'))
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE topology.diagram_layouts
|
||||||
|
ADD COLUMN IF NOT EXISTS theme_id uuid REFERENCES topology.diagram_themes(id);
|
||||||
@@ -3,6 +3,60 @@
|
|||||||
import json
|
import json
|
||||||
from topology_api.db import write_cursor
|
from topology_api.db import write_cursor
|
||||||
|
|
||||||
|
THEME_KEY = "guanghan_dark_engineering_v1"
|
||||||
|
|
||||||
|
THEME_TOKENS = {
|
||||||
|
"textColor": "#eaf2ff",
|
||||||
|
"mutedTextColor": "#91a3bd",
|
||||||
|
"metaColor": "#a6b5c9",
|
||||||
|
"hintColor": "#71849e",
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_STYLES = [
|
||||||
|
("node-card", "普通舱段", "#10263a", "#52baff", 3, 16),
|
||||||
|
("core-card", "核心舱", "#3b250d", "#ffad42", 5, 16),
|
||||||
|
("power-card", "电力模块分支", "#2a2810", "#d5b642", 3, 16),
|
||||||
|
("power-node-card", "电力模块中心", "#393412", "#d5b642", 5, 16),
|
||||||
|
("vehicle-card", "车辆", "#291c34", "#c28cdf", 3, 16),
|
||||||
|
]
|
||||||
|
|
||||||
|
EDGE_STYLES = [
|
||||||
|
("structural_mount", "结构直连", "#8fa1b8", 6, None, "square", "structural", 10),
|
||||||
|
("pressurized_passage", "加压走道", "#51e89c", 9, None, "square", "pressurized", 20),
|
||||||
|
("fuel", "燃料 / 供电", "#b178e6", 4, "13 10", "square", "utility", 30),
|
||||||
|
("power", "燃料 / 供电", "#b178e6", 4, "13 10", "square", "utility", 30),
|
||||||
|
("cooling", "冷却", "#78d6e6", 4, "10 8", "square", "cooling", 40),
|
||||||
|
("docking", "对接口", "#c28cdf", 4, "10 8", "square", "docking", 50),
|
||||||
|
]
|
||||||
|
|
||||||
|
DECORATION_STYLES = [
|
||||||
|
(
|
||||||
|
"radiator",
|
||||||
|
"大型折叠散热面板",
|
||||||
|
"#6b5814",
|
||||||
|
"#f0d869",
|
||||||
|
2,
|
||||||
|
"散热器",
|
||||||
|
"#f4df7b",
|
||||||
|
{
|
||||||
|
"widthRatio": 0.55,
|
||||||
|
"maxWidth": 130,
|
||||||
|
"height": 16,
|
||||||
|
"offsetXRatio": 0.55,
|
||||||
|
"offsetY": -18,
|
||||||
|
"ribCount": 3,
|
||||||
|
},
|
||||||
|
40,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
LEGEND_ITEMS = [
|
||||||
|
("structural", "结构直连", "edge", "structural_mount", 10),
|
||||||
|
("pressurized", "加压走道", "edge", "pressurized_passage", 20),
|
||||||
|
("utility", "燃料 / 供电", "edge", "fuel", 30),
|
||||||
|
("radiator", "大型折叠散热面板", "decoration", "radiator", 40),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def seed():
|
def seed():
|
||||||
with write_cursor() as cur:
|
with write_cursor() as cur:
|
||||||
@@ -10,19 +64,144 @@ def seed():
|
|||||||
cur.execute("SELECT id FROM bases WHERE code = 'guanghan'")
|
cur.execute("SELECT id FROM bases WHERE code = 'guanghan'")
|
||||||
base_id = cur.fetchone()["id"]
|
base_id = cur.fetchone()["id"]
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO diagram_themes
|
||||||
|
(id, base_id, theme_key, name, description, tokens, is_default)
|
||||||
|
VALUES (
|
||||||
|
gen_random_uuid(),
|
||||||
|
%(base)s,
|
||||||
|
%(theme_key)s,
|
||||||
|
'广寒深色工程图',
|
||||||
|
'广寒基地工程总图默认表现层主题',
|
||||||
|
%(tokens)s,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
ON CONFLICT (base_id, theme_key) DO UPDATE
|
||||||
|
SET name = EXCLUDED.name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
tokens = EXCLUDED.tokens,
|
||||||
|
is_default = true,
|
||||||
|
updated_at = now()
|
||||||
|
RETURNING id
|
||||||
|
""", {
|
||||||
|
"base": base_id,
|
||||||
|
"theme_key": THEME_KEY,
|
||||||
|
"tokens": json.dumps(THEME_TOKENS),
|
||||||
|
})
|
||||||
|
theme_id = cur.fetchone()["id"]
|
||||||
|
print(f"Theme: {theme_id}")
|
||||||
|
|
||||||
# Upsert layout
|
# Upsert layout
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO diagram_layouts (id, base_id, layout_key, name,
|
INSERT INTO diagram_layouts (id, base_id, layout_key, name,
|
||||||
canvas_width, canvas_height, is_default)
|
canvas_width, canvas_height, is_default, theme_id)
|
||||||
VALUES (gen_random_uuid(), %(base)s, 'engineering_overview_v1',
|
VALUES (gen_random_uuid(), %(base)s, 'engineering_overview_v1',
|
||||||
'工程总图 v1', 1500, 1900, true)
|
'工程总图 v1', 1500, 1900, true, %(theme)s)
|
||||||
ON CONFLICT (base_id, layout_key) DO UPDATE
|
ON CONFLICT (base_id, layout_key) DO UPDATE
|
||||||
SET name = EXCLUDED.name, is_default = true
|
SET name = EXCLUDED.name,
|
||||||
|
is_default = true,
|
||||||
|
theme_id = EXCLUDED.theme_id
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", {"base": base_id})
|
""", {"base": base_id, "theme": theme_id})
|
||||||
layout_id = cur.fetchone()["id"]
|
layout_id = cur.fetchone()["id"]
|
||||||
print(f"Layout: {layout_id}")
|
print(f"Layout: {layout_id}")
|
||||||
|
|
||||||
|
cur.execute("DELETE FROM diagram_legend_items WHERE theme_id = %(theme)s", {"theme": theme_id})
|
||||||
|
cur.execute("DELETE FROM diagram_decoration_styles WHERE theme_id = %(theme)s", {"theme": theme_id})
|
||||||
|
cur.execute("DELETE FROM diagram_edge_styles WHERE theme_id = %(theme)s", {"theme": theme_id})
|
||||||
|
cur.execute("DELETE FROM diagram_node_styles WHERE theme_id = %(theme)s", {"theme": theme_id})
|
||||||
|
|
||||||
|
for style_key, display_name, fill, stroke, stroke_width, corner_radius in NODE_STYLES:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO diagram_node_styles
|
||||||
|
(theme_id, style_key, display_name, fill_color,
|
||||||
|
stroke_color, stroke_width, corner_radius,
|
||||||
|
title_color, meta_color, hint_color)
|
||||||
|
VALUES
|
||||||
|
(%(theme)s, %(style_key)s, %(display_name)s, %(fill)s,
|
||||||
|
%(stroke)s, %(stroke_width)s, %(corner_radius)s,
|
||||||
|
%(title_color)s, %(meta_color)s, %(hint_color)s)
|
||||||
|
""", {
|
||||||
|
"theme": theme_id,
|
||||||
|
"style_key": style_key,
|
||||||
|
"display_name": display_name,
|
||||||
|
"fill": fill,
|
||||||
|
"stroke": stroke,
|
||||||
|
"stroke_width": stroke_width,
|
||||||
|
"corner_radius": corner_radius,
|
||||||
|
"title_color": THEME_TOKENS["textColor"],
|
||||||
|
"meta_color": THEME_TOKENS["metaColor"],
|
||||||
|
"hint_color": THEME_TOKENS["hintColor"],
|
||||||
|
})
|
||||||
|
|
||||||
|
for (
|
||||||
|
connection_type, display_name, stroke, stroke_width,
|
||||||
|
dash_array, line_cap, legend_group, legend_order,
|
||||||
|
) in EDGE_STYLES:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO diagram_edge_styles
|
||||||
|
(theme_id, connection_type, display_name, stroke_color,
|
||||||
|
stroke_width, dash_array, line_cap, legend_group,
|
||||||
|
legend_order)
|
||||||
|
VALUES
|
||||||
|
(%(theme)s, %(connection_type)s, %(display_name)s,
|
||||||
|
%(stroke)s, %(stroke_width)s, %(dash_array)s,
|
||||||
|
%(line_cap)s, %(legend_group)s, %(legend_order)s)
|
||||||
|
""", {
|
||||||
|
"theme": theme_id,
|
||||||
|
"connection_type": connection_type,
|
||||||
|
"display_name": display_name,
|
||||||
|
"stroke": stroke,
|
||||||
|
"stroke_width": stroke_width,
|
||||||
|
"dash_array": dash_array,
|
||||||
|
"line_cap": line_cap,
|
||||||
|
"legend_group": legend_group,
|
||||||
|
"legend_order": legend_order,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (
|
||||||
|
decoration_key, display_name, fill, stroke, stroke_width, label,
|
||||||
|
label_color, render_params, legend_order,
|
||||||
|
) in DECORATION_STYLES:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO diagram_decoration_styles
|
||||||
|
(theme_id, decoration_key, display_name, fill_color,
|
||||||
|
stroke_color, stroke_width, label, label_color,
|
||||||
|
render_params, legend_order)
|
||||||
|
VALUES
|
||||||
|
(%(theme)s, %(decoration_key)s, %(display_name)s,
|
||||||
|
%(fill)s, %(stroke)s, %(stroke_width)s, %(label)s,
|
||||||
|
%(label_color)s, %(render_params)s, %(legend_order)s)
|
||||||
|
""", {
|
||||||
|
"theme": theme_id,
|
||||||
|
"decoration_key": decoration_key,
|
||||||
|
"display_name": display_name,
|
||||||
|
"fill": fill,
|
||||||
|
"stroke": stroke,
|
||||||
|
"stroke_width": stroke_width,
|
||||||
|
"label": label,
|
||||||
|
"label_color": label_color,
|
||||||
|
"render_params": json.dumps(render_params),
|
||||||
|
"legend_order": legend_order,
|
||||||
|
})
|
||||||
|
|
||||||
|
for legend_key, label, item_type, style_ref, display_order in LEGEND_ITEMS:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO diagram_legend_items
|
||||||
|
(theme_id, legend_key, label, item_type,
|
||||||
|
style_ref, display_order, is_visible)
|
||||||
|
VALUES
|
||||||
|
(%(theme)s, %(legend_key)s, %(label)s, %(item_type)s,
|
||||||
|
%(style_ref)s, %(display_order)s, true)
|
||||||
|
""", {
|
||||||
|
"theme": theme_id,
|
||||||
|
"legend_key": legend_key,
|
||||||
|
"label": label,
|
||||||
|
"item_type": item_type,
|
||||||
|
"style_ref": style_ref,
|
||||||
|
"display_order": display_order,
|
||||||
|
})
|
||||||
|
|
||||||
# Component key → component_id mapping
|
# Component key → component_id mapping
|
||||||
cur.execute("SELECT component_key, id FROM components WHERE base_id = %(base)s",
|
cur.execute("SELECT component_key, id FROM components WHERE base_id = %(base)s",
|
||||||
{"base": base_id})
|
{"base": base_id})
|
||||||
|
|||||||
Reference in New Issue
Block a user