// element_ops.mbt - 要素操作の純粋関数
// 座標と接続情報をアトミックに更新する
///|
/// 要素移動の結果
pub(all) struct MoveResult {
elements : Array[Element]
}
// ============================================================
// BFS による接続ライン更新(共通関数)
// ============================================================
///|
/// BFS で接続された全てのラインを更新(純粋関数)
/// 指定した要素群から開始して、接続されている全てのラインを再帰的に更新する
/// except_ids に含まれる要素は更新対象から除外される
pub fn propagate_line_connections_bfs(
elements : Array[Element],
start_ids : Array[String],
except_ids? : Array[String] = [],
) -> Array[Element] {
// 座標を管理する Map(id -> (x, y, x2, y2))
let coords_map : Map[String, (Double, Double, Double, Double)] = {}
// 初期状態を設定
for e in elements {
if e.shape is Line(x2, y2) {
coords_map[e.id] = (e.x, e.y, x2, y2)
}
}
// BFS キュー
let queue : Array[String] = []
for id in start_ids {
queue.push(id)
}
let processed : Array[String] = []
while queue.length() > 0 {
let current_id = queue.remove(0)
if processed.contains(current_id) {
continue
}
processed.push(current_id)
// current_id の最新座標を取得
let current_el = elements.iter().find_first(fn(e) { e.id == current_id })
guard current_el is Some(target) else { continue }
// current_id に接続している Line を探して更新
for el in elements {
// 除外対象はスキップ
if except_ids.contains(el.id) {
continue
}
guard el.shape is Line(x2, y2) else { continue }
guard el.connections is Some(conns) else { continue }
// 既存の座標を取得
let (base_x, base_y, base_x2, base_y2) = coords_map
.get(el.id)
.unwrap_or((el.x, el.y, x2, y2))
let mut new_x = base_x
let mut new_y = base_y
let mut new_x2 = base_x2
let mut new_y2 = base_y2
let mut changed = false
// 始点が current_id に接続している場合
if conns.start is Some(conn) && conn.element_id == current_id {
let anchor_point = target.get_anchor_point(conn.anchor)
// Line の場合は更新された座標を使用
let p = if target.shape is Line(_, _) {
match coords_map.get(current_id) {
Some((cx, cy, cx2, cy2)) =>
get_anchor_from_line_coords(cx, cy, cx2, cy2, conn.anchor)
None => anchor_point
}
} else {
anchor_point
}
if p.x != new_x || p.y != new_y {
new_x = p.x
new_y = p.y
changed = true
}
}
// 終点が current_id に接続している場合
if conns.end is Some(conn) && conn.element_id == current_id {
let anchor_point = target.get_anchor_point(conn.anchor)
// Line の場合は更新された座標を使用
let p = if target.shape is Line(_, _) {
match coords_map.get(current_id) {
Some((cx, cy, cx2, cy2)) =>
get_anchor_from_line_coords(cx, cy, cx2, cy2, conn.anchor)
None => anchor_point
}
} else {
anchor_point
}
if p.x != new_x2 || p.y != new_y2 {
new_x2 = p.x
new_y2 = p.y
changed = true
}
}
if changed {
coords_map[el.id] = (new_x, new_y, new_x2, new_y2)
// この Line に接続している Line も更新が必要
if not(queue.contains(el.id)) && not(processed.contains(el.id)) {
queue.push(el.id)
}
}
}
}
// 更新を適用
elements.map(fn(el) {
guard el.shape is Line(_, _) else { return el }
match coords_map.get(el.id) {
Some((x, y, x2, y2)) =>
if x != el.x ||
y != el.y ||
(el.shape is Line(ox2, oy2) && (x2 != ox2 || y2 != oy2)) {
{ ..el, x, y, shape: Line(x2, y2) }
} else {
el
}
None => el
}
})
}
///|
/// 要素を移動し、関連する全ての更新を行う(純粋関数)
/// - 対象要素の座標を更新
/// - 子要素を同じ差分で移動
/// - 接続されているラインの端点を更新
pub fn move_element_with_relations(
elements : Array[Element],
id : String,
new_x : Double,
new_y : Double,
) -> Array[Element] {
// 対象要素を見つける
let mut target_el : Element? = None
for e in elements {
if e.id == id {
target_el = Some(e)
break
}
}
guard target_el is Some(el) else { return elements }
let dx = new_x - el.x
let dy = new_y - el.y
// Line の場合は特別な処理、それ以外は共通処理
match el.shape {
Line(_, _) =>
move_line_with_topology(elements, id, new_x, new_y, dx, dy, el)
// Path も他の図形と同様に接続ラインを更新
_ => move_shape_with_relations(elements, id, new_x, new_y, dx, dy, el)
}
}
///|
/// Line 以外の要素を移動(子要素と接続ラインも更新、BFS で全ホップを伝播)
fn move_shape_with_relations(
elements : Array[Element],
id : String,
new_x : Double,
new_y : Double,
dx : Double,
dy : Double,
_el : Element,
) -> Array[Element] {
// 子要素のIDを収集
let child_ids : Array[String] = []
for e in elements {
if e.parent_id == Some(id) {
child_ids.push(e.id)
}
}
// まず対象要素と子要素を移動
let updated = elements.map(fn(e) {
if e.id == id {
{ ..e, x: new_x, y: new_y }
} else if child_ids.contains(e.id) {
{ ..e, x: e.x + dx, y: e.y + dy }
} else {
e
}
})
// BFS で接続されている全てのラインを更新
propagate_line_connections_bfs(updated, [id])
}
///|
/// Line 要素を移動(接続グラフを更新)
fn move_line_with_topology(
elements : Array[Element],
id : String,
new_x : Double,
new_y : Double,
dx : Double,
dy : Double,
el : Element,
) -> Array[Element] {
// モデル層の純粋関数でトポロジー計算
let line_updates = compute_line_move_topology(
elements, id, new_x, new_y, dx, dy,
)
// 接続されている非 Line 要素のIDを収集
let connected_non_line_ids : Array[String] = []
match el.connections {
Some(conns) => {
match conns.start {
Some(conn) => {
let is_line = elements
.iter()
.any(fn(e) { e.id == conn.element_id && e.shape is Line(_, _) })
if not(is_line) &&
not(connected_non_line_ids.contains(conn.element_id)) {
connected_non_line_ids.push(conn.element_id)
}
}
None => ()
}
match conns.end {
Some(conn) => {
let is_line = elements
.iter()
.any(fn(e) { e.id == conn.element_id && e.shape is Line(_, _) })
if not(is_line) &&
not(connected_non_line_ids.contains(conn.element_id)) {
connected_non_line_ids.push(conn.element_id)
}
}
None => ()
}
}
None => ()
}
// 子要素を収集
let connected_children_ids : Array[String] = []
for connected_id in connected_non_line_ids {
for e in elements {
if e.parent_id == Some(connected_id) {
connected_children_ids.push(e.id)
}
}
}
// 全ての更新を適用
elements.map(fn(e) {
// 更新対象の Line
for coords in line_updates {
if e.id == coords.id {
return {
..e,
x: coords.x,
y: coords.y,
shape: Line(coords.x2, coords.y2),
}
}
}
// 接続された非 Line 要素を移動
if connected_non_line_ids.contains(e.id) {
return { ..e, x: e.x + dx, y: e.y + dy }
}
// 接続要素の子要素を移動
if connected_children_ids.contains(e.id) {
return { ..e, x: e.x + dx, y: e.y + dy }
}
e
})
}
///|
/// 要素のサイズ変更後に関連要素を更新(純粋関数、BFS で全ホップを伝播)
/// - 子要素を親の中央に再配置
/// - 接続されているラインの端点を更新
pub fn resize_element_with_relations(
elements : Array[Element],
id : String,
new_shape : ShapeType,
) -> Array[Element] {
// 対象要素を見つける
let mut target_el : Element? = None
for e in elements {
if e.id == id {
target_el = Some(e)
break
}
}
guard target_el is Some(el) else { return elements }
// 新しい形状で要素を更新
let updated_el = { ..el, shape: new_shape }
// 新しいバウンディングボックスの中央を計算
let bbox = updated_el.bounding_box()
let center_x = bbox.x + bbox.width / 2.0
let center_y = bbox.y + bbox.height / 2.0
// まず対象要素と子要素を更新
let updated = elements.map(fn(e) {
if e.id == id {
{ ..e, shape: new_shape }
} else if e.parent_id == Some(id) {
{ ..e, x: center_x, y: center_y }
} else {
e
}
})
// BFS で接続されている全てのラインを更新
propagate_line_connections_bfs(updated, [id])
}