// Scene - Signal ベースの要素管理

// =============================================================================
// 定数
// =============================================================================

///|
/// 要素の最小サイズ(リサイズ時の下限)
let min_element_size : Double = 10.0

///|
/// 配置オフセット(同一座標への連続配置時)
let placement_offset : Double = 15.0

///|
/// アンカースナップ距離(Line端点のアンカースナップ判定)
let anchor_snap_threshold : Double = 20.0

// =============================================================================
// 型定義
// =============================================================================

///|
/// エディタモード
pub(all) enum EditorMode {
  Full // フル機能モード
  Embedded // 埋め込み簡易モード
}

///|
/// ツールモード
pub(all) enum ToolMode {
  Select // 選択モード(デフォルト)
  FreeDraw // フリードローモード
} derive(Show, Eq)

///|
/// フリードロー状態(描画中の点群)
pub(all) struct FreeDrawState {
  points : Array[@model.Point]
}

///|
/// フリードローオプション
pub(all) struct FreeDrawOptions {
  resample_dist : Double // リサンプリング間隔(デフォルト 4.0)
  epsilon : Double // RDP 簡略化パラメータ(デフォルト 2.0)
  max_error : Double // ベジェフィッティング最大誤差(デフォルト 8.0)
}

///|
/// 線のスナップ先プレビュー(ドラッグ中の視覚的フィードバック用)
pub(all) struct PendingConnection {
  target_id : String // 接続先の要素 ID
  anchor : @model.Anchor // アンカー位置
  point : @model.Point // 座標
}

///|
/// 配置モード状態(ショートカットキーからの要素挿入時)
pub(all) struct PlacementState {
  element_id : String // 配置中の要素ID
  anchor_element_id : String // アンカー元の要素ID
  anchor : @model.Anchor // アンカー位置
  anchor_point : @model.Point // アンカー座標
  is_line : Bool // Line/Arrow かどうか(接続情報を保存するか)
  is_arrow : Bool // Arrow かどうか(矢印マーカーを追加するか)
  has_moved : Bool // 1フレームでも動いたかどうか(Arrow表示判定用)
}

///|
/// アンカータイプ(アンカーポイントの位置)
pub(all) enum AnchorType {
  Top
  Bottom
  Left
  Right
  Center
  LineStart
  LineEnd
}

///|
/// アンカードラッグ状態(アンカーから線を描画中)
pub(all) struct AnchorDragState {
  source_element_id : String // ドラッグ開始元の要素ID
  source_anchor_type : AnchorType // どのアンカーから始まったか
  source_x : Double // アンカーポイントのX座標
  source_y : Double // アンカーポイントのY座標
  current_x : Double // 現在のマウスX座標
  current_y : Double // 現在のマウスY座標
}

///|
extern "js" fn js_null() -> @js.Any =
  #| () => null

///|
/// エディタの状態
pub struct EditorState {
  width : Int // 初期ビューポート幅
  height : Int // 初期ビューポート高さ
  canvas_width : @luna.Signal[Int] // キャンバス幅(動的)
  canvas_height : @luna.Signal[Int] // キャンバス高さ(動的)
  doc_width : @luna.Signal[Double] // ドキュメント幅
  doc_height : @luna.Signal[Double] // ドキュメント高さ
  elements : @luna.Signal[Array[@model.Element]] // 要素リスト
  selected_ids : @luna.Signal[Array[String]] // 選択中の要素 ID リスト(複数選択対応)
  drag_state : @luna.Signal[@model.DragState?] // ドラッグ状態
  multi_drag_state : @luna.Signal[@model.MultiDragState?] // 複数要素ドラッグ状態
  resize_state : @luna.Signal[@model.ResizeState?] // リサイズ状態
  box_select : @luna.Signal[@model.BoxSelectState?] // 矩形選択状態
  context_menu : @luna.Signal[@model.ContextMenu?] // コンテキストメニュー
  viewport : @luna.Signal[@model.Viewport] // ビューポート
  grid_enabled : @luna.Signal[Bool] // グリッドスナップ有効
  grid_size : @luna.Signal[Int] // グリッドサイズ(ピクセル)
  is_panning : @luna.Signal[Bool] // パン中かどうか
  pan_start : @luna.Signal[(Double, Double)?] // パン開始位置(スクリーン座標)
  text_edit : @luna.Signal[@model.TextEditState?] // テキスト編集状態
  pending_connection : @luna.Signal[PendingConnection?] // ドラッグ中の接続プレビュー
  placement_state : @luna.Signal[PlacementState?] // 配置モード状態
  mouse_scene_pos : @luna.Signal[@model.Point] // マウス位置(シーン座標)
  mouse_in_canvas : @luna.Signal[Bool] // マウスがキャンバス内にいるか
  preview_bg : @luna.Signal[String] // プレビュー背景色
  embed_bg : @luna.Signal[String] // 埋め込み時の背景色(デフォルト: transparent)
  theme_mode : @luna.Signal[@model.ThemeMode] // テーマモード
  is_mobile : @luna.Signal[Bool] // モバイル表示かどうか
  panel_visible : @luna.Signal[Bool] // パネル表示/非表示
  mode : EditorMode // エディタモード
  modal_open : @luna.Signal[Bool] // モーダル表示状態(埋め込みモード用)
  // 埋め込みモード用の追加フィールド
  has_focus : @luna.Signal[Bool] // フォーカスがあるか(キーボードイベント制御用)
  is_readonly : @luna.Signal[Bool] // 読み取り専用モード
  show_toolbar : @luna.Signal[Bool] // ツールバー表示/非表示
  // 埋め込みモード用 DOM 参照
  anchor_drag_state : @luna.Signal[AnchorDragState?] // アンカードラッグ状態
  svg_el : @luna.Signal[@js.Any] // SVG 要素への参照
  container_el : @luna.Signal[@js.Any] // コンテナ要素への参照
  // フリードロー関連
  tool_mode : @luna.Signal[ToolMode] // ツールモード(Select / FreeDraw)
  free_draw_state : @luna.Signal[FreeDrawState?] // フリードロー中の状態
  free_draw_options : @luna.Signal[FreeDrawOptions] // フリードローオプション
  // 配置位置オフセット
  last_placement_pos : @luna.Signal[@model.Point?] // 最後に配置した位置
  // ピンチズーム用(マルチタッチ)
  pinch_state : @luna.Signal[PinchState?] // ピンチズーム状態
  // バリデーション結果
  validation_result : @luna.Signal[@model.ValidationResult?] // 検証結果
  // プレーンSVG要素のID(移動のみ可能)
  plain_svg_ids : @luna.Signal[Array[String]] // プレーンSVGとしてインポートされた要素
}

///|
/// ピンチズーム状態
pub struct PinchState {
  pointer1_id : Int // 最初のポインター ID
  pointer2_id : Int // 2番目のポインター ID
  pointer1_pos : @model.Point // 最初のポインター位置
  pointer2_pos : @model.Point // 2番目のポインター位置
  initial_distance : Double // 初期距離
  initial_zoom : Double // 初期ズーム
  center : @model.Point // ピンチ中心(シーン座標)
}

///|
/// エディタ状態を作成
pub fn EditorState::new(width : Int, height : Int) -> EditorState {
  // 初期ドキュメントサイズ(キャンバスと同じ)
  let doc_w = 400.0
  let doc_h = 300.0
  // 初期 zoom は 100%、ドキュメント左上から表示
  let initial_viewport : @model.Viewport = {
    scroll_x: 0.0,
    scroll_y: 0.0,
    zoom: 1.0,
  }
  {
    width,
    height,
    canvas_width: @luna.signal(width),
    canvas_height: @luna.signal(height),
    doc_width: @luna.signal(doc_w),
    doc_height: @luna.signal(doc_h),
    elements: @luna.signal([]),
    selected_ids: @luna.signal([]),
    drag_state: @luna.signal(None),
    multi_drag_state: @luna.signal(None),
    resize_state: @luna.signal(None),
    box_select: @luna.signal(None),
    context_menu: @luna.signal(None),
    viewport: @luna.signal(initial_viewport),
    grid_enabled: @luna.signal(false),
    grid_size: @luna.signal(20),
    is_panning: @luna.signal(false),
    pan_start: @luna.signal(None),
    text_edit: @luna.signal(None),
    pending_connection: @luna.signal(None),
    placement_state: @luna.signal(None),
    mouse_scene_pos: @luna.signal({ x: 0.0, y: 0.0 }),
    mouse_in_canvas: @luna.signal(false),
    preview_bg: @luna.signal("#ffffff"),
    embed_bg: @luna.signal("transparent"),
    theme_mode: @luna.signal(@model.Light),
    is_mobile: @luna.signal(width < 768),
    panel_visible: @luna.signal(true),
    mode: Full,
    modal_open: @luna.signal(false),
    // 埋め込みモード用
    has_focus: @luna.signal(true), // Full モードではデフォルトで true
    is_readonly: @luna.signal(false),
    show_toolbar: @luna.signal(true),
    // 埋め込みモード用 DOM 参照
    anchor_drag_state: @luna.signal(None),
    svg_el: @luna.signal(js_null()),
    container_el: @luna.signal(js_null()),
    // フリードロー関連
    tool_mode: @luna.signal(Select),
    free_draw_state: @luna.signal(None),
    free_draw_options: @luna.signal({
      resample_dist: 4.0,
      epsilon: 2.0,
      max_error: 8.0,
    }),
    // 配置位置オフセット
    last_placement_pos: @luna.signal(None),
    pinch_state: @luna.signal(None),
    // バリデーション
    validation_result: @luna.signal(None),
    plain_svg_ids: @luna.signal([]),
  }
}

///|
/// 指定モードでエディタ状態を作成
pub fn EditorState::new_with_mode(
  width : Int,
  height : Int,
  mode : EditorMode,
) -> EditorState {
  let state = EditorState::new(width, height)
  { ..state, mode, }
}

///|
/// 現在のテーマを取得
pub fn EditorState::get_theme(self : EditorState) -> @model.Theme {
  match self.theme_mode.get() {
    @model.Light => @model.Theme::light()
    @model.Dark => @model.Theme::dark()
  }
}

///|
/// テーマに基づいたデフォルトスタイルを取得(CSS変数を使用)
pub fn EditorState::get_default_style(_self : EditorState) -> @model.Style {
  {
    fill: Some(@model.css_var_fill),
    stroke: Some(@model.css_var_stroke),
    stroke_width: Some(1.0),
    opacity: None,
    stroke_dasharray: None,
    marker_start: None,
    marker_end: None,
    font_family: None,
  }
}

///|
/// テーマに基づいたテキストスタイルを取得(CSS変数を使用)
pub fn EditorState::get_text_style(_self : EditorState) -> @model.Style {
  {
    fill: Some(@model.css_var_text),
    stroke: None,
    stroke_width: None,
    opacity: None,
    stroke_dasharray: None,
    marker_start: None,
    marker_end: None,
    font_family: None,
  }
}

///|
/// Line用のスタイルを取得(CSS変数を使用)
pub fn EditorState::get_line_style(_self : EditorState) -> @model.Style {
  {
    fill: None,
    stroke: Some(@model.css_var_stroke),
    stroke_width: Some(1.0),
    opacity: None,
    stroke_dasharray: None,
    marker_start: None,
    marker_end: None,
    font_family: None,
  }
}

///|
/// Arrow用のスタイルを取得(CSS変数を使用)
pub fn EditorState::get_arrow_style(_self : EditorState) -> @model.Style {
  {
    fill: None,
    stroke: Some(@model.css_var_stroke),
    stroke_width: Some(1.0),
    opacity: None,
    stroke_dasharray: None,
    marker_start: None,
    marker_end: Some(@model.Arrow),
    font_family: None,
  }
}

///|
/// Arrow用のスタイルを取得(矢印なし - 配置モード初期用)
pub fn EditorState::get_arrow_style_initial(
  _self : EditorState,
) -> @model.Style {
  {
    fill: None,
    stroke: Some(@model.css_var_stroke),
    stroke_width: Some(1.0),
    opacity: None,
    stroke_dasharray: None,
    marker_start: None,
    marker_end: None, // 初期は矢印なし
    font_family: None,
  }
}

///|
/// テキスト編集を開始(新規作成)
pub fn EditorState::start_text_edit(
  self : EditorState,
  parent_id : String,
  x : Double,
  y : Double,
) -> Unit {
  self.text_edit.set(
    Some({
      parent_id,
      x,
      y,
      editing_id: None,
      initial_text: "",
      font_size: None,
    }),
  )
}

///|
/// テキスト編集を開始(既存テキストの編集)
pub fn EditorState::start_text_edit_existing(
  self : EditorState,
  text_id : String,
  parent_id : String,
  x : Double,
  y : Double,
  initial_text : String,
  font_size : Double?,
) -> Unit {
  self.text_edit.set(
    Some({ parent_id, x, y, editing_id: Some(text_id), initial_text, font_size }),
  )
}

///|
/// テキスト編集を終了
pub fn EditorState::end_text_edit(self : EditorState) -> Unit {
  self.text_edit.set(None)
}

///|
/// グリッドスナップを切り替え
pub fn EditorState::toggle_grid(self : EditorState) -> Unit {
  self.grid_enabled.update(fn(v) { not(v) })
}

///|
/// 座標をグリッドにスナップ
pub fn EditorState::snap_to_grid(
  self : EditorState,
  x : Double,
  y : Double,
) -> (Double, Double) {
  @model.snap_to_grid(x, y, self.grid_size.get(), self.grid_enabled.get())
}

///|
/// 要素を追加(無効な要素は追加しない)
pub fn EditorState::add_element(
  self : EditorState,
  element : @model.Element,
) -> Unit {
  // サイズ0の図形やPathは追加しない
  guard element.is_valid() else { return }
  self.elements.update(fn(els) {
    let new_els = els.copy()
    new_els.push(element)
    new_els
  })
}

///|
/// 要素を ID で検索
pub fn EditorState::find_element(
  self : EditorState,
  id : String,
) -> @model.Element? {
  for el in self.elements.get() {
    if el.id == id {
      return Some(el)
    }
  }
  None
}

///|
/// 指定IDの要素が存在するかチェック
pub fn EditorState::has_element_with_id(
  self : EditorState,
  id : String,
) -> Bool {
  for el in self.elements.get() {
    if el.id == id {
      return true
    }
  }
  false
}

///|
/// 要素のIDを変更(衝突チェック付き)
/// 成功時は true、ID衝突時は false を返す
pub fn EditorState::rename_element_id(
  self : EditorState,
  old_id : String,
  new_id : String,
) -> Bool {
  // 同じIDなら何もしない
  if old_id == new_id {
    return true
  }
  // 新しいIDが既に存在する場合は失敗
  if self.has_element_with_id(new_id) {
    return false
  }
  // IDを更新
  self.elements.update(fn(els) {
    els.map(fn(el) { if el.id == old_id { { ..el, id: new_id } } else { el } })
  })
  // 選択状態も更新
  self.selected_ids.update(fn(ids) {
    ids.map(fn(id) { if id == old_id { new_id } else { id } })
  })
  true
}

///|
/// 指定要素の子要素を取得
pub fn EditorState::get_children(
  self : EditorState,
  parent_id : String,
) -> Array[@model.Element] {
  let children : Array[@model.Element] = []
  for el in self.elements.get() {
    if el.parent_id == Some(parent_id) {
      children.push(el)
    }
  }
  children
}

///|
/// 要素が子要素かどうか判定
pub fn EditorState::is_child_element(self : EditorState, id : String) -> Bool {
  if self.find_element(id) is Some(el) {
    el.parent_id is Some(_)
  } else {
    false
  }
}

///|
/// 子要素の親を取得
pub fn EditorState::get_parent_id(self : EditorState, id : String) -> String? {
  if self.find_element(id) is Some(el) {
    el.parent_id
  } else {
    None
  }
}

///|
/// 要素を更新
pub fn EditorState::update_element(
  self : EditorState,
  id : String,
  updater : (@model.Element) -> @model.Element,
) -> Unit {
  self.elements.update(fn(els) {
    els.map(fn(el) { if el.id == id { updater(el) } else { el } })
  })
}

///|
/// 要素を移動(子要素も一緒に移動、接続ラインも更新)
/// model 層の純粋関数を使用してアトミックに更新
pub fn EditorState::move_element(
  self : EditorState,
  id : String,
  new_x : Double,
  new_y : Double,
) -> Unit {
  // 座標を整数に丸める(データ可読性向上のため)
  let rounded_x = @model.round_coord(new_x)
  let rounded_y = @model.round_coord(new_y)
  // model 層の純粋関数で全ての更新を計算し、1回の set で適用
  let new_elements = @model.move_element_with_relations(
    self.elements.get(),
    id,
    rounded_x,
    rounded_y,
  )
  self.elements.set(new_elements)
}

///|
/// 指定座標にある要素を検索(上から順に)
pub fn EditorState::hit_test(
  self : EditorState,
  x : Double,
  y : Double,
) -> @model.Element? {
  @model.find_element_at(self.elements.get(), { x, y })
}

///|
/// 要素を選択(単一)
pub fn EditorState::select(self : EditorState, id : String?) -> Unit {
  match id {
    Some(id) => self.selected_ids.set([id])
    None => self.selected_ids.set([])
  }
}

///|
/// 複数要素を選択
pub fn EditorState::select_multiple(
  self : EditorState,
  ids : Array[String],
) -> Unit {
  self.selected_ids.set(ids)
}

///|
/// 選択に追加
pub fn EditorState::add_to_selection(self : EditorState, id : String) -> Unit {
  let current = self.selected_ids.get()
  if not(current.contains(id)) {
    let new_ids = current.copy()
    new_ids.push(id)
    self.selected_ids.set(new_ids)
  }
}

///|
/// 選択から除外
pub fn EditorState::remove_from_selection(
  self : EditorState,
  id : String,
) -> Unit {
  self.selected_ids.update(fn(ids) { ids.filter(fn(i) { i != id }) })
}

///|
/// 選択されているか判定
pub fn EditorState::is_selected(self : EditorState, id : String) -> Bool {
  self.selected_ids.get().contains(id)
}

///|
/// 最初の選択要素を取得(後方互換性用)
pub fn EditorState::get_selected_id(self : EditorState) -> String? {
  let ids = self.selected_ids.get()
  if ids.length() > 0 {
    Some(ids[0])
  } else {
    None
  }
}

///|
/// 指定IDの要素を更新(汎用ヘルパー)
pub fn EditorState::update_element_by_id(
  self : EditorState,
  id : String,
  updater : (@model.Element) -> @model.Element,
) -> Unit {
  self.elements.update(fn(els) {
    els.map(fn(el) { if el.id == id { updater(el) } else { el } })
  })
}

///|
/// 全要素を選択(Line と子要素以外)
pub fn EditorState::select_all(self : EditorState) -> Unit {
  let ids : Array[String] = []
  for el in self.elements.get() {
    // 子要素は親と一緒に選択されるのでスキップ
    if el.parent_id is Some(_) {
      continue
    }
    // Line/Arrow は接続先と一緒に選択されることが多いので含める
    ids.push(el.id)
  }
  self.selected_ids.set(ids)
}

///|
/// ドラッグ開始
pub fn EditorState::start_drag(
  self : EditorState,
  element_id : String,
  mouse_x : Double,
  mouse_y : Double,
) -> Unit {
  // 要素が見つからない場合は何もしない(無効な要素は add_element_raw でスキップされる)
  guard self.find_element(element_id) is Some(el) else { return }
  // Line の場合は終点と接続情報、接続要素の位置も保存
  let (start_shape, start_connections, connected_positions) : (
    @model.ShapeType?,
    @model.LineConnections?,
    Array[(String, Double, Double)],
  ) = match el.shape {
    @model.Line(_, _) => {
      let positions : Array[(String, Double, Double)] = []
      match el.connections {
        Some(conns) => {
          match conns.start {
            Some(conn) =>
              if self.find_element(conn.element_id) is Some(connected_el) {
                positions.push(
                  (conn.element_id, connected_el.x, connected_el.y),
                )
              }
            None => ()
          }
          match conns.end {
            Some(conn) =>
              if self.find_element(conn.element_id) is Some(connected_el) {
                positions.push(
                  (conn.element_id, connected_el.x, connected_el.y),
                )
              }
            None => ()
          }
        }
        None => ()
      }
      (Some(el.shape), el.connections, positions)
    }
    _ => (None, None, [])
  }
  // Path も通常の要素と同様に el.x, el.y(transform オフセット)を基準にする
  let offset_x = mouse_x - el.x
  let offset_y = mouse_y - el.y
  self.drag_state.set(
    Some({
      element_id,
      start_x: el.x,
      start_y: el.y,
      offset_x,
      offset_y,
      start_shape,
      start_connections,
      connected_start_positions: connected_positions,
    }),
  )
}

///|
/// ドラッグ中の移動
pub fn EditorState::drag_move(
  self : EditorState,
  mouse_x : Double,
  mouse_y : Double,
) -> Unit {
  guard self.drag_state.get() is Some(state)
  let pos = @model.calc_drag_position(
    state.offset_x,
    state.offset_y,
    mouse_x,
    mouse_y,
  )
  let (snapped_x, snapped_y) = self.snap_to_grid(pos.x, pos.y)
  self.move_element(state.element_id, snapped_x, snapped_y)
}

///|
/// ドラッグ終了
pub fn EditorState::end_drag(self : EditorState) -> Unit {
  // ドラッグ終了時、座標を整数に丸める(SVG の可読性向上)
  if self.drag_state.get() is Some(state) {
    self.elements.update(fn(els) {
      els.map(fn(el) {
        if el.id == state.element_id {
          @model.round_element_coords(el)
        } else {
          el
        }
      })
    })
  }
  self.drag_state.set(None)
}

///|
/// ドラッグ中かどうか
pub fn EditorState::is_dragging(self : EditorState) -> Bool {
  self.drag_state.get() is Some(_)
}

///|
/// コンテキストメニューを表示
pub fn EditorState::show_context_menu(
  self : EditorState,
  x : Double,
  y : Double,
  scene_x : Double,
  scene_y : Double,
  target_id : String?,
) -> Unit {
  self.context_menu.set(Some({ x, y, scene_x, scene_y, target_id }))
}

///|
/// コンテキストメニューを非表示
pub fn EditorState::hide_context_menu(self : EditorState) -> Unit {
  self.context_menu.set(None)
}

///|
/// 要素を削除
pub fn EditorState::remove_element(self : EditorState, id : String) -> Unit {
  self.elements.update(fn(els) { els.filter(fn(el) { el.id != id }) })
  // 選択中の要素が削除されたら選択解除
  self.remove_from_selection(id)
}

///|
/// 選択中の要素を削除(親子関係を考慮)
/// - 親要素を削除すると子要素も削除
/// - 子要素を削除すると親要素も削除
pub fn EditorState::delete_selected(self : EditorState) -> Unit {
  let ids = self.selected_ids.get().copy()
  let all_ids : Array[String] = []
  let elements = self.elements.get()

  for id in ids {
    all_ids.push(id)
    // 子要素を収集
    for el in elements {
      if el.parent_id == Some(id) {
        all_ids.push(el.id)
      }
    }
    // 親要素を収集
    if self.find_element(id) is Some(el) {
      if el.parent_id is Some(parent_id) {
        all_ids.push(parent_id)
        // 親の他の子も収集(親を削除するので)
        for child in elements {
          if child.parent_id == Some(parent_id) {
            all_ids.push(child.id)
          }
        }
      }
    }
  }

  // 重複を除去して削除
  let deleted : Array[String] = []
  for id in all_ids {
    if not(deleted.contains(id)) {
      deleted.push(id)
      self.remove_element(id)
    }
  }
}

// ============================================================
// 履歴管理用の低レベル操作(_raw サフィックス)
// これらは直接履歴に記録せず、Command から呼び出される
// ============================================================

///|
/// 要素を追加(履歴なし、無効な要素は追加しない)
pub fn EditorState::add_element_raw(
  self : EditorState,
  element : @model.Element,
) -> Unit {
  // サイズ0の図形やPathは追加しない
  guard element.is_valid() else { return }
  self.elements.update(fn(els) {
    let new_els = els.copy()
    new_els.push(element)
    new_els
  })
}

///|
/// 要素を削除(履歴なし、子要素も削除)
pub fn EditorState::remove_element_raw(self : EditorState, id : String) -> Unit {
  // 要素とその子要素を削除
  self.elements.update(fn(els) {
    els.filter(fn(el) { el.id != id && el.parent_id != Some(id) })
  })
  // 選択から除外
  self.remove_from_selection(id)
}

///|
/// 要素の現在のインデックスを取得
pub fn EditorState::get_element_index(self : EditorState, id : String) -> Int? {
  let els = self.elements.get()
  for i = 0; i < els.length(); i = i + 1 {
    if els[i].id == id {
      return Some(i)
    }
  }
  None
}

///|
/// 要素を指定位置に移動(履歴なし)
pub fn EditorState::reorder_element_raw(
  self : EditorState,
  id : String,
  to_index : Int,
) -> Unit {
  self.elements.update(fn(els) {
    let mut from_idx = -1
    for i = 0; i < els.length(); i = i + 1 {
      if els[i].id == id {
        from_idx = i
        break
      }
    }
    if from_idx < 0 {
      return els
    }
    let el = els[from_idx]
    let new_els : Array[@model.Element] = []
    // 要素を除いた配列を作成
    for i = 0; i < els.length(); i = i + 1 {
      if i != from_idx {
        new_els.push(els[i])
      }
    }
    // 正しい位置に挿入
    let insert_idx = if to_index > new_els.length() {
      new_els.length()
    } else if to_index < 0 {
      0
    } else {
      to_index
    }
    let result : Array[@model.Element] = []
    for i = 0; i < new_els.length(); i = i + 1 {
      if i == insert_idx {
        result.push(el)
      }
      result.push(new_els[i])
    }
    if insert_idx >= new_els.length() {
      result.push(el)
    }
    result
  })
}

///|
/// 要素のスタイルを更新(履歴なし)
pub fn EditorState::update_style_raw(
  self : EditorState,
  id : String,
  new_style : @model.Style,
) -> Unit {
  self.elements.update(fn(els) {
    els.map(fn(el) { if el.id == id { { ..el, style: new_style } } else { el } })
  })
}

///|
/// 要素のIDを変更(履歴なし、衝突チェックなし)
pub fn EditorState::rename_element_raw(
  self : EditorState,
  old_id : String,
  new_id : String,
) -> Unit {
  // IDを更新
  self.elements.update(fn(els) {
    els.map(fn(el) { if el.id == old_id { { ..el, id: new_id } } else { el } })
  })
  // 選択状態も更新
  self.selected_ids.update(fn(ids) {
    ids.map(fn(id) { if id == old_id { new_id } else { id } })
  })
}

///|
/// テキスト要素の内容を更新(履歴なし)
pub fn EditorState::update_text_raw(
  self : EditorState,
  id : String,
  new_content : String,
) -> Unit {
  self.elements.update(fn(els) {
    els.map(fn(el) {
      if el.id == id {
        match el.shape {
          @model.Text(_, font_size) =>
            { ..el, shape: @model.Text(new_content, font_size) }
          _ => el
        }
      } else {
        el
      }
    })
  })
}

///|
/// 要素を1つ前面に移動
pub fn EditorState::bring_forward(
  self : EditorState,
  id : String,
) -> (Int, Int)? {
  let els = self.elements.get()
  if self.get_element_index(id) is Some(from_idx) {
    let to_idx = from_idx + 1
    if to_idx < els.length() {
      self.reorder_element_raw(id, to_idx)
      return Some((from_idx, to_idx))
    }
  }
  None
}

///|
/// 要素を最前面に移動
pub fn EditorState::bring_to_front(
  self : EditorState,
  id : String,
) -> (Int, Int)? {
  let els = self.elements.get()
  if self.get_element_index(id) is Some(from_idx) {
    let to_idx = els.length() - 1
    if from_idx != to_idx {
      self.reorder_element_raw(id, to_idx)
      return Some((from_idx, to_idx))
    }
  }
  None
}

///|
/// 要素を1つ背面に移動
pub fn EditorState::send_backward(
  self : EditorState,
  id : String,
) -> (Int, Int)? {
  if self.get_element_index(id) is Some(from_idx) {
    if from_idx > 0 {
      let to_idx = from_idx - 1
      self.reorder_element_raw(id, to_idx)
      return Some((from_idx, to_idx))
    }
  }
  None
}

///|
/// 要素を最背面に移動
pub fn EditorState::send_to_back(
  self : EditorState,
  id : String,
) -> (Int, Int)? {
  if self.get_element_index(id) is Some(from_idx) {
    if from_idx != 0 {
      self.reorder_element_raw(id, 0)
      return Some((from_idx, 0))
    }
  }
  None
}

// ============================================================
// ズーム/パン操作
// ============================================================

///|
/// ズームイン
pub fn EditorState::zoom_in(self : EditorState) -> Unit {
  self.viewport.update(fn(vp) {
    let new_zoom = vp.zoom * 1.01
    // 最大 400%
    let clamped = if new_zoom > 4.0 { 4.0 } else { new_zoom }
    { ..vp, zoom: clamped }
  })
}

///|
/// ズームアウト
pub fn EditorState::zoom_out(self : EditorState) -> Unit {
  self.viewport.update(fn(vp) {
    let new_zoom = vp.zoom / 1.01
    // 最小 25%
    let clamped = if new_zoom < 0.25 { 0.25 } else { new_zoom }
    { ..vp, zoom: clamped }
  })
}

///|
/// 指定位置を基準にズーム(マウス位置中心)
pub fn EditorState::zoom_at(
  self : EditorState,
  screen_x : Double,
  screen_y : Double,
  zoom_in : Bool,
) -> Unit {
  self.viewport.update(fn(vp) {
    // 現在のマウス位置のシーン座標を取得
    let scene = vp.screen_to_scene(screen_x, screen_y)
    // 新しいズーム値を計算(1%刻み)
    let factor = if zoom_in { 1.01 } else { 1.0 / 1.01 }
    let new_zoom = vp.zoom * factor
    // 範囲制限
    let clamped = if new_zoom > 4.0 {
      4.0
    } else if new_zoom < 0.25 {
      0.25
    } else {
      new_zoom
    }
    // ズーム後も同じシーン座標がマウス位置に来るようにスクロール調整
    // scene.x = screen_x / clamped + new_scroll_x
    // new_scroll_x = scene.x - screen_x / clamped
    let new_scroll_x = scene.x - screen_x / clamped
    let new_scroll_y = scene.y - screen_y / clamped
    { zoom: clamped, scroll_x: new_scroll_x, scroll_y: new_scroll_y }
  })
}

///|
/// ピンチズームを開始
pub fn EditorState::start_pinch(
  self : EditorState,
  pointer1_id : Int,
  pointer2_id : Int,
  p1 : @model.Point,
  p2 : @model.Point,
) -> Unit {
  let dx = p2.x - p1.x
  let dy = p2.y - p1.y
  let distance = (dx * dx + dy * dy).sqrt()
  let vp = self.viewport.get()
  // ピンチ中心のスクリーン座標
  let center_screen_x = (p1.x + p2.x) / 2.0
  let center_screen_y = (p1.y + p2.y) / 2.0
  // スクリーン座標からシーン座標に変換
  let center_scene = vp.screen_to_scene(center_screen_x, center_screen_y)
  self.pinch_state.set(
    Some({
      pointer1_id,
      pointer2_id,
      pointer1_pos: p1,
      pointer2_pos: p2,
      initial_distance: distance,
      initial_zoom: vp.zoom,
      center: center_scene,
    }),
  )
}

///|
/// ピンチズームを更新
pub fn EditorState::update_pinch(
  self : EditorState,
  pointer_id : Int,
  pos : @model.Point,
) -> Unit {
  guard self.pinch_state.get() is Some(state) else { return }
  // ポインター位置を更新
  let (p1, p2) = if pointer_id == state.pointer1_id {
    (pos, state.pointer2_pos)
  } else if pointer_id == state.pointer2_id {
    (state.pointer1_pos, pos)
  } else {
    return
  }
  // 新しい距離を計算
  let dx = p2.x - p1.x
  let dy = p2.y - p1.y
  let new_distance = (dx * dx + dy * dy).sqrt()
  // 距離の比率からズーム値を計算
  let scale = new_distance / state.initial_distance
  let new_zoom = state.initial_zoom * scale
  // 範囲制限
  let clamped = if new_zoom > 4.0 {
    4.0
  } else if new_zoom < 0.25 {
    0.25
  } else {
    new_zoom
  }
  // ピンチ中心のスクリーン座標
  let center_screen_x = (p1.x + p2.x) / 2.0
  let center_screen_y = (p1.y + p2.y) / 2.0
  // ズーム後も同じシーン座標がピンチ中心に来るようにスクロール調整
  let new_scroll_x = state.center.x - center_screen_x / clamped
  let new_scroll_y = state.center.y - center_screen_y / clamped
  self.viewport.set({
    zoom: clamped,
    scroll_x: new_scroll_x,
    scroll_y: new_scroll_y,
  })
  // 状態を更新
  self.pinch_state.set(Some({ ..state, pointer1_pos: p1, pointer2_pos: p2 }))
}

///|
/// ピンチズームを終了
pub fn EditorState::end_pinch(self : EditorState, pointer_id : Int) -> Unit {
  guard self.pinch_state.get() is Some(state) else { return }
  // いずれかのポインターがリリースされたらピンチ終了
  if pointer_id == state.pointer1_id || pointer_id == state.pointer2_id {
    self.pinch_state.set(None)
  }
}

///|
/// ピンチ中かどうか
pub fn EditorState::is_pinching(self : EditorState) -> Bool {
  self.pinch_state.get() is Some(_)
}

///|
/// ビューポートをリセット
pub fn EditorState::reset_viewport(self : EditorState) -> Unit {
  self.fit_to_canvas()
}

///|
/// ドキュメント全体がキャンバスにフィットする zoom を計算してセット
pub fn EditorState::fit_to_canvas(self : EditorState) -> Unit {
  let doc_w = self.doc_width.get()
  let doc_h = self.doc_height.get()
  let canvas_w = self.canvas_width.get().to_double()
  let canvas_h = self.canvas_height.get().to_double()
  // 余白を含めてフィット
  let margin = 40.0
  let zoom_x = canvas_w / (doc_w + margin * 2.0)
  let zoom_y = canvas_h / (doc_h + margin * 2.0)
  let fit_zoom = if zoom_x < zoom_y { zoom_x } else { zoom_y }
  // ドキュメントを中央に配置
  let vb_width = canvas_w / fit_zoom
  let vb_height = canvas_h / fit_zoom
  let scroll_x = -(vb_width - doc_w) / 2.0
  let scroll_y = -(vb_height - doc_h) / 2.0
  self.viewport.set({ scroll_x, scroll_y, zoom: fit_zoom })
}

///|
/// ビューポートの中央座標を取得(シーン座標)
pub fn EditorState::get_viewport_center(self : EditorState) -> @model.Point {
  let vp = self.viewport.get()
  let canvas_w = self.canvas_width.get().to_double()
  let canvas_h = self.canvas_height.get().to_double()
  let center_x = vp.scroll_x + canvas_w / vp.zoom / 2.0
  let center_y = vp.scroll_y + canvas_h / vp.zoom / 2.0
  { x: center_x, y: center_y }
}

///|
/// 要素配置位置を取得(マウスがキャンバス内ならマウス位置、そうでなければ画面中央)
/// 同じ座標に繰り返し配置する場合は +15px ずらす
pub fn EditorState::get_placement_position(self : EditorState) -> @model.Point {
  let base_pos = if self.mouse_in_canvas.get() {
    self.mouse_scene_pos.get()
  } else {
    self.get_viewport_center()
  }
  // 最後の配置位置と同じなら offset ずらす
  let offset = placement_offset
  let final_pos = match self.last_placement_pos.get() {
    Some(last) =>
      if (base_pos.x - last.x).abs() < 1.0 && (base_pos.y - last.y).abs() < 1.0 {
        // 同じ位置なら右下にオフセット
        @model.Point::{
          x: @model.round_coord(last.x + offset),
          y: @model.round_coord(last.y + offset),
        }
      } else {
        // 座標を整数に丸める
        @model.Point::{
          x: @model.round_coord(base_pos.x),
          y: @model.round_coord(base_pos.y),
        }
      }
    None =>
      // 座標を整数に丸める
      @model.Point::{
        x: @model.round_coord(base_pos.x),
        y: @model.round_coord(base_pos.y),
      }
  }
  // 位置を記録
  self.last_placement_pos.set(Some(final_pos))
  final_pos
}

///|
/// パン(スクロール)
pub fn EditorState::pan(self : EditorState, dx : Double, dy : Double) -> Unit {
  self.viewport.update(fn(vp) {
    { ..vp, scroll_x: vp.scroll_x + dx, scroll_y: vp.scroll_y + dy }
  })
}

///|
/// パン開始
pub fn EditorState::start_pan(
  self : EditorState,
  screen_x : Double,
  screen_y : Double,
) -> Unit {
  self.is_panning.set(true)
  self.pan_start.set(Some((screen_x, screen_y)))
}

///|
/// パン移動
pub fn EditorState::pan_move(
  self : EditorState,
  screen_x : Double,
  screen_y : Double,
) -> Unit {
  if self.pan_start.get() is Some((start_x, start_y)) {
    let vp = self.viewport.get()
    // スクリーン座標での移動量をシーン座標に変換
    let dx = (start_x - screen_x) / vp.zoom
    let dy = (start_y - screen_y) / vp.zoom
    self.viewport.update(fn(v) {
      { ..v, scroll_x: v.scroll_x + dx, scroll_y: v.scroll_y + dy }
    })
    self.pan_start.set(Some((screen_x, screen_y)))
  }
}

///|
/// パン終了
pub fn EditorState::end_pan(self : EditorState) -> Unit {
  self.is_panning.set(false)
  self.pan_start.set(None)
}

// ============================================================
// リサイズ操作
// ============================================================

///|
/// リサイズ開始
pub fn EditorState::start_resize(
  self : EditorState,
  element_id : String,
  handle : @model.HandlePosition,
  mouse_x : Double,
  mouse_y : Double,
) -> Unit {
  // 要素が見つからない場合は何もしない(無効な要素は add_element_raw でスキップされる)
  guard self.find_element(element_id) is Some(el) else { return }
  self.resize_state.set(
    Some({
      element_id,
      handle,
      start_x: el.x,
      start_y: el.y,
      start_shape: el.shape,
      mouse_start_x: mouse_x,
      mouse_start_y: mouse_y,
    }),
  )
}

///|
/// リサイズ中の移動
pub fn EditorState::resize_move(
  self : EditorState,
  mouse_x : Double,
  mouse_y : Double,
) -> Unit {
  guard self.resize_state.get() is Some(state)
  let dx = mouse_x - state.mouse_start_x
  let dy = mouse_y - state.mouse_start_y
  // 単一値のグリッドスナップ(グリッド有効時のみ丸める)
  let snap = fn(v : Double) -> Double {
    if self.grid_enabled.get() {
      let size = self.grid_size.get().to_double()
      (v / size).round() * size
    } else {
      v // グリッド無効時はそのまま
    }
  }
  // ハンドル位置に応じて新しい位置と形状を計算
  match state.start_shape {
    @model.Rect(w, h, rx, ry) => {
      // 矩形の4隅を計算
      let x1 = state.start_x // 左
      let y1 = state.start_y // 上
      let x2 = state.start_x + w // 右
      let y2 = state.start_y + h // 下
      // ドラッグ中のハンドルに応じて、動かす点をスナップ、固定点は維持
      let (new_x1, new_y1, new_x2, new_y2) = match state.handle {
        @model.SE => {
          // SE(右下)をドラッグ:左上固定、右下をスナップ
          let dragged_x = snap(x2 + dx)
          let dragged_y = snap(y2 + dy)
          (x1, y1, dragged_x, dragged_y)
        }
        @model.NW => {
          // NW(左上)をドラッグ:右下固定、左上をスナップ
          let dragged_x = snap(x1 + dx)
          let dragged_y = snap(y1 + dy)
          (dragged_x, dragged_y, x2, y2)
        }
        @model.NE => {
          // NE(右上)をドラッグ:左下固定、右上をスナップ
          let dragged_x = snap(x2 + dx)
          let dragged_y = snap(y1 + dy)
          (x1, dragged_y, dragged_x, y2)
        }
        @model.SW => {
          // SW(左下)をドラッグ:右上固定、左下をスナップ
          let dragged_x = snap(x1 + dx)
          let dragged_y = snap(y2 + dy)
          (dragged_x, y1, x2, dragged_y)
        }
        _ => (x1, y1, x2 + dx, y2 + dy)
      }
      // 最小サイズを保証(幅・高さが負にならないように)
      let min_size = min_element_size
      let (final_x1, final_x2) = if new_x2 - new_x1 < min_size {
        match state.handle {
          @model.NW | @model.SW => (new_x2 - min_size, new_x2)
          _ => (new_x1, new_x1 + min_size)
        }
      } else {
        (new_x1, new_x2)
      }
      let (final_y1, final_y2) = if new_y2 - new_y1 < min_size {
        match state.handle {
          @model.NW | @model.NE => (new_y2 - min_size, new_y2)
          _ => (new_y1, new_y1 + min_size)
        }
      } else {
        (new_y1, new_y2)
      }
      let final_x = final_x1
      let final_y = final_y1
      let final_w = final_x2 - final_x1
      let final_h = final_y2 - final_y1
      // 子要素の新しい中央位置
      let child_center_x = final_x + final_w / 2.0
      let child_center_y = final_y + final_h / 2.0
      self.elements.update(fn(els) {
        els.map(fn(el) {
          if el.id == state.element_id {
            {
              ..el,
              x: final_x,
              y: final_y,
              shape: @model.Rect(final_w, final_h, rx, ry),
            }
          } else if el.parent_id == Some(state.element_id) {
            // 子要素を親の中央に配置
            { ..el, x: child_center_x, y: child_center_y }
          } else {
            el
          }
        })
      })
      // 接続されているラインを更新
      self.update_connected_lines(state.element_id)
    }
    @model.Circle(r) => {
      // 円の場合:中心は固定、ドラッグ点(辺)をスナップして半径を計算
      let cx = state.start_x
      let cy = state.start_y
      // ドラッグ中の辺の位置を計算
      let (edge_x, edge_y) = match state.handle {
        @model.SE => (cx + r + dx, cy + r + dy)
        @model.NW => (cx - r + dx, cy - r + dy)
        @model.NE => (cx + r + dx, cy - r + dy)
        @model.SW => (cx - r + dx, cy + r + dy)
        _ => (cx + r + dx, cy + r + dy)
      }
      // スナップ
      let snapped_edge_x = snap(edge_x)
      let snapped_edge_y = snap(edge_y)
      // 中心からの距離で半径を計算(x, y の平均を使用)
      let dist_x = (snapped_edge_x - cx).abs()
      let dist_y = (snapped_edge_y - cy).abs()
      let new_r = (dist_x + dist_y) / 2.0
      let final_r = if new_r < 5.0 { 5.0 } else { new_r }
      self.elements.update(fn(els) {
        els.map(fn(el) {
          if el.id == state.element_id {
            { ..el, shape: @model.Circle(final_r) }
          } else if el.parent_id == Some(state.element_id) {
            // 子要素を親の中央に配置(円の中心)
            { ..el, x: cx, y: cy }
          } else {
            el
          }
        })
      })
      // 接続されているラインを更新
      self.update_connected_lines(state.element_id)
    }
    @model.Ellipse(rx, ry) => {
      // 楕円の場合:中心は固定、ドラッグ点をスナップして rx, ry を計算
      let cx = state.start_x
      let cy = state.start_y
      // 楕円のバウンディングボックスの4隅
      let x1 = cx - rx
      let y1 = cy - ry
      let x2 = cx + rx
      let y2 = cy + ry
      // ドラッグ中のハンドルに応じて、動かす点をスナップ
      let (new_x1, new_y1, new_x2, new_y2) = match state.handle {
        @model.SE => {
          let dragged_x = snap(x2 + dx)
          let dragged_y = snap(y2 + dy)
          (x1, y1, dragged_x, dragged_y)
        }
        @model.NW => {
          let dragged_x = snap(x1 + dx)
          let dragged_y = snap(y1 + dy)
          (dragged_x, dragged_y, x2, y2)
        }
        @model.NE => {
          let dragged_x = snap(x2 + dx)
          let dragged_y = snap(y1 + dy)
          (x1, dragged_y, dragged_x, y2)
        }
        @model.SW => {
          let dragged_x = snap(x1 + dx)
          let dragged_y = snap(y2 + dy)
          (dragged_x, y1, x2, dragged_y)
        }
        _ => (x1, y1, x2 + dx, y2 + dy)
      }
      // 新しい中心と半径を計算
      let new_cx = (new_x1 + new_x2) / 2.0
      let new_cy = (new_y1 + new_y2) / 2.0
      let new_rx = (new_x2 - new_x1) / 2.0
      let new_ry = (new_y2 - new_y1) / 2.0
      // 最小サイズを保証
      let final_rx = if new_rx < 5.0 { 5.0 } else { new_rx }
      let final_ry = if new_ry < 5.0 { 5.0 } else { new_ry }
      self.elements.update(fn(els) {
        els.map(fn(el) {
          if el.id == state.element_id {
            {
              ..el,
              x: new_cx,
              y: new_cy,
              shape: @model.Ellipse(final_rx, final_ry),
            }
          } else if el.parent_id == Some(state.element_id) {
            // 子要素を親の中央に配置(楕円の中心)
            { ..el, x: new_cx, y: new_cy }
          } else {
            el
          }
        })
      })
      // 接続されているラインを更新
      self.update_connected_lines(state.element_id)
    }
    @model.Line(x2, y2) => {
      // 線の端点をドラッグ(アンカースナップ判定用)
      let snap_threshold = anchor_snap_threshold
      match state.handle {
        @model.LineStart => {
          // 始点を移動
          let raw_x = state.start_x + dx
          let raw_y = state.start_y + dy
          // 近くのアンカーポイントを探す(アンカースナップが優先)
          let snap_result = self.find_nearest_connection_point(
            { x: raw_x, y: raw_y },
            [state.element_id],
            snap_threshold,
          )
          let (new_x, new_y) = match snap_result {
            Some((_, _, snap_point)) =>
              // アンカースナップが優先
              (snap_point.x, snap_point.y)
            None =>
              // アンカーがなければグリッドスナップ
              (snap(raw_x), snap(raw_y))
          }
          // pending_connection を更新
          match snap_result {
            Some((target_el, anchor, snap_point)) =>
              self.pending_connection.set(
                Some({ target_id: target_el.id, anchor, point: snap_point }),
              )
            None => self.pending_connection.set(None)
          }
          self.elements.update(fn(els) {
            els.map(fn(el) {
              if el.id == state.element_id {
                { ..el, x: new_x, y: new_y }
              } else {
                el
              }
            })
          })
          // この Line に接続されているラインを更新
          self.update_connected_lines(state.element_id)
        }
        @model.LineEnd => {
          // 終点を移動(Line の x2, y2 は絶対座標)
          // 新しい終点の絶対座標を計算
          let raw_end_x = x2 + dx
          let raw_end_y = y2 + dy
          // 近くのアンカーポイントを探す(アンカースナップが優先)
          let snap_result = self.find_nearest_connection_point(
            { x: raw_end_x, y: raw_end_y },
            [state.element_id],
            snap_threshold,
          )
          let (final_x2, final_y2) = match snap_result {
            Some((_, _, snap_point)) => (snap_point.x, snap_point.y)
            None =>
              // アンカーがなければグリッドスナップ
              (snap(raw_end_x), snap(raw_end_y))
          }
          // pending_connection を更新
          match snap_result {
            Some((target_el, anchor, snap_point)) =>
              self.pending_connection.set(
                Some({ target_id: target_el.id, anchor, point: snap_point }),
              )
            None => self.pending_connection.set(None)
          }
          self.elements.update(fn(els) {
            els.map(fn(el) {
              if el.id == state.element_id {
                { ..el, shape: @model.Line(final_x2, final_y2) }
              } else {
                el
              }
            })
          })
          // この Line に接続されているラインを更新
          self.update_connected_lines(state.element_id)
        }
        _ => () // Line に対して NW/NE/SW/SE ハンドルは無効
      }
    }
    _ => () // 他の形状は未対応
  }
}

///|
/// リサイズ終了
pub fn EditorState::end_resize(self : EditorState) -> Unit {
  // Line の場合、pending_connection があれば接続情報を保存
  if self.resize_state.get() is Some(resize) {
    if self.find_element(resize.element_id) is Some(el) {
      if el.shape is @model.Line(_, _) {
        if self.pending_connection.get() is Some(pending) {
          // 現在の接続情報を取得または新規作成
          let current_conns = el.connections.unwrap_or(
            @model.LineConnections::none(),
          )
          // ハンドル位置に応じて始点または終点の接続を更新
          let new_conns = match resize.handle {
            @model.LineStart => {
              let conn : @model.Connection = {
                element_id: pending.target_id,
                anchor: pending.anchor,
              }
              { ..current_conns, start: Some(conn) }
            }
            @model.LineEnd => {
              let conn : @model.Connection = {
                element_id: pending.target_id,
                anchor: pending.anchor,
              }
              { ..current_conns, end: Some(conn) }
            }
            _ => current_conns
          }
          self.update_line_connection(resize.element_id, new_conns)
        } else {
          // スナップしていない場合は接続を解除
          let current_conns = el.connections.unwrap_or(
            @model.LineConnections::none(),
          )
          let new_conns = match resize.handle {
            @model.LineStart => { ..current_conns, start: None }
            @model.LineEnd => { ..current_conns, end: None }
            _ => current_conns
          }
          // 接続が両方なくなった場合は connections を None に
          if new_conns.start is None && new_conns.end is None {
            self.elements.update(fn(els) {
              els.map(fn(e) {
                if e.id == resize.element_id {
                  { ..e, connections: None }
                } else {
                  e
                }
              })
            })
          } else {
            self.update_line_connection(resize.element_id, new_conns)
          }
        }
      } else {
        // 親要素がリサイズされた場合、子テキストの位置を中央に調整
        self.center_child_elements(resize.element_id)
      }
      // 長さゼロの線は削除
      if el.shape is @model.Line(x2, y2) {
        let dx = x2 - el.x
        let dy = y2 - el.y
        let length = (dx * dx + dy * dy).sqrt()
        if length < 5.0 {
          self.remove_element(resize.element_id)
          self.pending_connection.set(None)
          self.resize_state.set(None)
          self.select(None)
          return
        }
      }
    }
  }
  // リサイズ完了後、座標を整数に丸める(SVG の可読性向上)
  if self.resize_state.get() is Some(resize) {
    self.elements.update(fn(els) {
      els.map(fn(el) {
        if el.id == resize.element_id {
          @model.round_element_coords(el)
        } else {
          el
        }
      })
    })
  }
  // リサイズ完了後、要素を選択状態にする(ハンドルを表示するため)
  if self.resize_state.get() is Some(resize) {
    self.select(Some(resize.element_id))
  }
  self.pending_connection.set(None)
  self.resize_state.set(None)
}

///|
/// 親要素の子テキストを中央に配置
pub fn EditorState::center_child_elements(
  self : EditorState,
  parent_id : String,
) -> Unit {
  let elements = self.elements.get()
  // 親要素を検索
  let parent_el : @model.Element? = {
    let mut found : @model.Element? = None
    for e in elements {
      if e.id == parent_id {
        found = Some(e)
        break
      }
    }
    found
  }
  guard parent_el is Some(parent) else { return }

  // 親の中央座標を計算
  let bbox = parent.bounding_box()
  let center_x = bbox.x + bbox.width / 2.0
  let center_y = bbox.y + bbox.height / 2.0

  // 子要素の位置を更新
  self.elements.update(fn(els) {
    els.map(fn(el) {
      if el.parent_id == Some(parent_id) {
        { ..el, x: center_x, y: center_y }
      } else {
        el
      }
    })
  })
}

///|
/// リサイズ中かどうか
pub fn EditorState::is_resizing(self : EditorState) -> Bool {
  self.resize_state.get() is Some(_)
}

// ============================================================
// コマンド実行(Undo/Redo)
// ============================================================

///|
/// コマンドを実行して履歴に追加
pub fn execute_command(
  history : @model.History,
  state : EditorState,
  command : @model.Command,
) -> Unit {
  // コマンドを実行
  apply_command(command, state)
  // Undo スタックに追加
  history.undo_stack.push(command)
  // Redo スタックをクリア(新しい操作後は Redo できない)
  history.redo_stack.clear()
}

///|
/// Undo(取り消し)
pub fn undo_command(history : @model.History, state : EditorState) -> Bool {
  match history.undo_stack.pop() {
    None => false
    Some(command) => {
      // 逆操作を実行
      unapply_command(command, state)
      // Redo スタックに追加
      history.redo_stack.push(command)
      true
    }
  }
}

///|
/// Redo(やり直し)
pub fn redo_command(history : @model.History, state : EditorState) -> Bool {
  match history.redo_stack.pop() {
    None => false
    Some(command) => {
      // 操作を再実行
      apply_command(command, state)
      // Undo スタックに追加
      history.undo_stack.push(command)
      true
    }
  }
}

///|
/// コマンドを適用(実行)
fn apply_command(command : @model.Command, state : EditorState) -> Unit {
  match command {
    @model.AddElement(element) => state.add_element_raw(element)
    @model.RemoveElement(element) => state.remove_element_raw(element.id)
    @model.MoveElement(id, _, _, to_x, to_y) => {
      // model 層の純粋関数でアトミックに更新
      let new_elements = @model.move_element_with_relations(
        state.elements.get(),
        id,
        to_x,
        to_y,
      )
      state.elements.set(new_elements)
    }
    @model.ResizeElement(id, _, new_shape) => {
      // model 層の純粋関数でアトミックに更新
      let new_elements = @model.resize_element_with_relations(
        state.elements.get(),
        id,
        new_shape,
      )
      state.elements.set(new_elements)
    }
    @model.ReorderElement(id, _, to_idx) =>
      state.reorder_element_raw(id, to_idx)
    @model.UpdateStyle(id, _, new_style) =>
      state.update_style_raw(id, new_style)
    @model.RenameElement(old_id, new_id) =>
      state.rename_element_raw(old_id, new_id)
    @model.UpdateText(id, _, new_content) =>
      state.update_text_raw(id, new_content)
  }
}

///|
/// コマンドを取り消し(逆操作)
fn unapply_command(command : @model.Command, state : EditorState) -> Unit {
  match command {
    @model.AddElement(element) => state.remove_element_raw(element.id)
    @model.RemoveElement(element) => state.add_element_raw(element)
    @model.MoveElement(id, from_x, from_y, _, _) => {
      // model 層の純粋関数でアトミックに更新
      let new_elements = @model.move_element_with_relations(
        state.elements.get(),
        id,
        from_x,
        from_y,
      )
      state.elements.set(new_elements)
    }
    @model.ResizeElement(id, old_shape, _) => {
      // model 層の純粋関数でアトミックに更新
      let new_elements = @model.resize_element_with_relations(
        state.elements.get(),
        id,
        old_shape,
      )
      state.elements.set(new_elements)
    }
    @model.ReorderElement(id, from_idx, _) =>
      state.reorder_element_raw(id, from_idx)
    @model.UpdateStyle(id, old_style, _) =>
      state.update_style_raw(id, old_style)
    @model.RenameElement(old_id, new_id) =>
      state.rename_element_raw(new_id, old_id)
    @model.UpdateText(id, old_content, _) =>
      state.update_text_raw(id, old_content)
  }
}

// ============================================================
// 矩形選択
// ============================================================

///|
/// 矩形選択を開始
pub fn EditorState::start_box_select(
  self : EditorState,
  x : Double,
  y : Double,
) -> Unit {
  self.box_select.set(
    Some({ start_x: x, start_y: y, current_x: x, current_y: y }),
  )
}

///|
/// 矩形選択を更新
pub fn EditorState::update_box_select(
  self : EditorState,
  x : Double,
  y : Double,
) -> Unit {
  guard self.box_select.get() is Some(state)
  self.box_select.set(Some({ ..state, current_x: x, current_y: y }))
}

///|
/// 矩形選択を終了し、範囲内の要素を選択
pub fn EditorState::end_box_select(self : EditorState) -> Unit {
  if self.box_select.get() is Some(state) {
    let bbox = state.to_bbox()
    // 一定サイズ以上の選択範囲の場合のみ処理
    if bbox.width > 5.0 || bbox.height > 5.0 {
      let elements = @model.find_elements_in_box(self.elements.get(), bbox)
      let ids = elements.map(fn(el) { el.id })
      self.select_multiple(ids)
    }
  }
  self.box_select.set(None)
}

///|
/// 矩形選択中かどうか
pub fn EditorState::is_box_selecting(self : EditorState) -> Bool {
  self.box_select.get() is Some(_)
}

// ============================================================
// 複数要素ドラッグ
// ============================================================

///|
/// 複数要素のドラッグを開始
pub fn EditorState::start_multi_drag(
  self : EditorState,
  mouse_x : Double,
  mouse_y : Double,
) -> Unit {
  let ids = self.selected_ids.get()
  if ids.length() == 0 {
    return
  }
  // 各要素の開始位置を記録
  let start_positions : Array[(String, Double, Double)] = []
  let line_endpoints : Array[(String, Double, Double)] = []
  for id in ids {
    if self.find_element(id) is Some(el) {
      start_positions.push((id, el.x, el.y))
      // Line の場合は終点も記録
      if el.shape is @model.Line(x2, y2) {
        line_endpoints.push((id, x2, y2))
      }
    }
  }
  self.multi_drag_state.set(
    Some({
      element_ids: ids,
      start_positions,
      line_endpoints,
      mouse_start_x: mouse_x,
      mouse_start_y: mouse_y,
    }),
  )
}

///|
/// 複数要素のドラッグ中の移動
pub fn EditorState::multi_drag_move(
  self : EditorState,
  mouse_x : Double,
  mouse_y : Double,
) -> Unit {
  guard self.multi_drag_state.get() is Some(state)
  let dx = mouse_x - state.mouse_start_x
  let dy = mouse_y - state.mouse_start_y
  let selected_ids = state.element_ids
  // Line の初期終点を検索するヘルパー
  let find_initial_endpoint = fn(id : String) -> (Double, Double)? {
    for ep in state.line_endpoints {
      let (ep_id, x2, y2) = ep
      if ep_id == id {
        return Some((x2, y2))
      }
    }
    None
  }
  // 各要素を移動
  for pos in state.start_positions {
    let (id, start_x, start_y) = pos
    guard self.find_element(id) is Some(el) else { continue }
    // Line の場合は接続を考慮
    match el.shape {
      @model.Line(_, _) => {
        // 接続情報を確認
        let conns = el.connections.unwrap_or(@model.LineConnections::none())
        // 始点が選択外の要素に接続されているか
        let start_connected_outside = match conns.start {
          Some(conn) => not(selected_ids.contains(conn.element_id))
          None => false
        }
        // 終点が選択外の要素に接続されているか
        let end_connected_outside = match conns.end {
          Some(conn) => not(selected_ids.contains(conn.element_id))
          None => false
        }
        if start_connected_outside && end_connected_outside {
          // 両端が選択外に接続 → 移動しない
          continue
        } else if start_connected_outside {
          // 始点が選択外に接続 → 終点だけ移動(初期値から計算)
          guard find_initial_endpoint(id) is Some((init_x2, init_y2)) else {
            continue
          }
          let new_x2 = init_x2 + dx
          let new_y2 = init_y2 + dy
          self.elements.update(fn(els) {
            els.map(fn(e) {
              if e.id == id {
                { ..e, shape: @model.Line(new_x2, new_y2) }
              } else {
                e
              }
            })
          })
        } else if end_connected_outside {
          // 終点が選択外に接続 → 始点だけ移動(x2, y2 は絶対座標なのでそのまま)
          guard find_initial_endpoint(id) is Some((init_x2, init_y2)) else {
            continue
          }
          let new_x = start_x + dx
          let new_y = start_y + dy
          let (snapped_x, snapped_y) = self.snap_to_grid(new_x, new_y)
          self.elements.update(fn(els) {
            els.map(fn(e) {
              if e.id == id {
                {
                  ..e,
                  x: snapped_x,
                  y: snapped_y,
                  shape: @model.Line(init_x2, init_y2),
                }
              } else {
                e
              }
            })
          })
        } else {
          // 両端とも選択外に接続していない → 通常移動
          let new_x = start_x + dx
          let new_y = start_y + dy
          let (snapped_x, snapped_y) = self.snap_to_grid(new_x, new_y)
          self.move_element(id, snapped_x, snapped_y)
        }
      }
      _ => {
        // Line 以外は通常移動
        let new_x = start_x + dx
        let new_y = start_y + dy
        let (snapped_x, snapped_y) = self.snap_to_grid(new_x, new_y)
        self.move_element(id, snapped_x, snapped_y)
      }
    }
  }
}

///|
/// 複数要素のドラッグを終了
pub fn EditorState::end_multi_drag(self : EditorState) -> Unit {
  // ドラッグ終了時、座標を整数に丸める(SVG の可読性向上)
  if self.multi_drag_state.get() is Some(state) {
    let ids = state.element_ids
    self.elements.update(fn(els) {
      els.map(fn(el) {
        if ids.contains(el.id) {
          @model.round_element_coords(el)
        } else {
          el
        }
      })
    })
  }
  self.multi_drag_state.set(None)
}

///|
/// 複数要素ドラッグ中かどうか
pub fn EditorState::is_multi_dragging(self : EditorState) -> Bool {
  self.multi_drag_state.get() is Some(_)
}

// ============================================================
// 関連要素の同期
// ============================================================

///|
/// 要素の変更後に関連する要素を同期更新
/// - 子要素(テキストなど)を親の中央に再配置
/// - 接続されているラインの端点を更新
pub fn EditorState::sync_related_elements(
  self : EditorState,
  element_id : String,
) -> Unit {
  self.center_child_elements(element_id)
  self.update_connected_lines(element_id)
}

// ============================================================
// 接続ライン管理
// ============================================================

///|
/// 指定要素に接続されている全てのラインを更新(BFS で全ホップを伝播)
pub fn EditorState::update_connected_lines(
  self : EditorState,
  element_id : String,
) -> Unit {
  // model 層の BFS 関数を使用
  self.elements.update(fn(elements) {
    @model.propagate_line_connections_bfs(elements, [element_id])
  })
}

///|
/// 指定要素に接続されているラインを更新(特定のラインを除く、BFS で全ホップを伝播)
pub fn EditorState::update_connected_lines_except(
  self : EditorState,
  element_id : String,
  except_line_id : String,
) -> Unit {
  // model 層の BFS 関数を使用(except_ids で除外)
  self.elements.update(fn(elements) {
    @model.propagate_line_connections_bfs(elements, [element_id], except_ids=[
      except_line_id,
    ])
  })
}

///|
/// 線の接続情報を更新
pub fn EditorState::update_line_connection(
  self : EditorState,
  line_id : String,
  connections : @model.LineConnections,
) -> Unit {
  self.elements.update(fn(els) {
    els.map(fn(el) {
      if el.id == line_id {
        { ..el, connections: Some(connections) }
      } else {
        el
      }
    })
  })
}

///|
/// 指定座標から最も近い接続ポイントを探す(全要素から)
pub fn EditorState::find_nearest_connection_point(
  self : EditorState,
  point : @model.Point,
  exclude_ids : Array[String],
  threshold : Double,
) -> (@model.Element, @model.Anchor, @model.Point)? {
  // 純粋関数を呼び出し、結果を変換
  match
    @model.find_nearest_connection(
      self.elements.get(),
      point,
      exclude_ids,
      threshold,
    ) {
    Some(result) => Some((result.element, result.anchor, result.point))
    None => None
  }
}

// ============================================================
// 配置モード関数
// ============================================================

///|
/// 配置モードを開始
pub fn EditorState::start_placement(
  self : EditorState,
  element_id : String,
  anchor_element_id : String,
  anchor : @model.Anchor,
  anchor_point : @model.Point,
  is_line : Bool,
  is_arrow? : Bool = false,
) -> Unit {
  self.placement_state.set(
    Some({
      element_id,
      anchor_element_id,
      anchor,
      anchor_point,
      is_line,
      is_arrow,
      has_moved: false,
    }),
  )
  self.select(Some(element_id))
}

///|
/// 配置モード中のマウス位置に応じて要素を更新
pub fn EditorState::update_placement(
  self : EditorState,
  mouse_pos : @model.Point,
) -> Unit {
  guard self.placement_state.get() is Some(placement)
  guard self.find_element(placement.element_id) is Some(el)

  // 初回移動時に has_moved を true に設定し、Arrow のマーカーを有効化
  if not(placement.has_moved) {
    self.placement_state.set(Some({ ..placement, has_moved: true }))
    // Arrow の場合、マーカーを追加
    if placement.is_arrow {
      self.update_element(placement.element_id, fn(e) {
        { ..e, style: { ..e.style, marker_end: Some(@model.Arrow) } }
      })
    }
  }

  // 単一値のグリッドスナップ(グリッド有効時のみ丸める)
  let snap = fn(v : Double) -> Double {
    if self.grid_enabled.get() {
      let size = self.grid_size.get().to_double()
      (v / size).round() * size
    } else {
      v // グリッド無効時はそのまま
    }
  }
  let anchor_x = placement.anchor_point.x
  let anchor_y = placement.anchor_point.y
  let mouse_x = snap(mouse_pos.x)
  let mouse_y = snap(mouse_pos.y)
  match el.shape {
    @model.Line(_, _) =>
      // Line/Arrow: 終点をマウス位置に更新(始点はアンカー位置で固定)
      self.update_element(placement.element_id, fn(e) {
        { ..e, shape: @model.Line(mouse_x, mouse_y) }
      })
    @model.Circle(_) => {
      // Circle: アンカーを中心として、マウス距離で半径を決定
      let dx = mouse_x - anchor_x
      let dy = mouse_y - anchor_y
      let raw_radius = (dx * dx + dy * dy).sqrt()
      let radius = if raw_radius < 5.0 { 5.0 } else { raw_radius }
      self.update_element(placement.element_id, fn(e) {
        { ..e, x: anchor_x, y: anchor_y, shape: @model.Circle(radius) }
      })
    }
    @model.Ellipse(_, _) => {
      // Ellipse: アンカーを中心として、マウス位置で rx/ry を決定
      let raw_rx = (mouse_x - anchor_x).abs()
      let raw_ry = (mouse_y - anchor_y).abs()
      let rx = if raw_rx < 5.0 { 5.0 } else { raw_rx }
      let ry = if raw_ry < 5.0 { 5.0 } else { raw_ry }
      self.update_element(placement.element_id, fn(e) {
        { ..e, x: anchor_x, y: anchor_y, shape: @model.Ellipse(rx, ry) }
      })
    }
    @model.Rect(_, _, corner_rx, corner_ry) => {
      // Rect: アンカーを固定点として、マウス位置で対角を決定
      let min_x = if anchor_x < mouse_x { anchor_x } else { mouse_x }
      let min_y = if anchor_y < mouse_y { anchor_y } else { mouse_y }
      let raw_width = (mouse_x - anchor_x).abs()
      let raw_height = (mouse_y - anchor_y).abs()
      let width = if raw_width < 10.0 { 10.0 } else { raw_width }
      let height = if raw_height < 10.0 { 10.0 } else { raw_height }
      self.update_element(placement.element_id, fn(e) {
        {
          ..e,
          x: min_x,
          y: min_y,
          shape: @model.Rect(width, height, corner_rx, corner_ry),
        }
      })
    }
    @model.Text(content, font_size) =>
      // Text: マウス位置に配置
      self.update_element(placement.element_id, fn(e) {
        { ..e, x: mouse_x, y: mouse_y, shape: @model.Text(content, font_size) }
      })
    _ =>
      // その他: マウス位置に配置
      self.update_element(placement.element_id, fn(e) {
        { ..e, x: mouse_x, y: mouse_y }
      })
  }
}

///|
/// 配置モードを終了(確定)
pub fn EditorState::end_placement(self : EditorState) -> Unit {
  guard self.placement_state.get() is Some(placement)
  guard self.find_element(placement.element_id) is Some(el)

  // 長さゼロの線は削除
  if el.shape is @model.Line(x2, y2) {
    let dx = x2 - el.x
    let dy = y2 - el.y
    let length = (dx * dx + dy * dy).sqrt()
    if length < 5.0 {
      // 最小長さ未満は削除
      self.remove_element(placement.element_id)
      self.placement_state.set(None)
      self.select(None)
      return
    }
  }

  // Line/Arrow の場合は接続情報を保存
  if placement.is_line {
    let connections : @model.LineConnections = {
      start: Some({
        element_id: placement.anchor_element_id,
        anchor: placement.anchor,
      }),
      end: None,
    }
    self.update_line_connection(placement.element_id, connections)
  }
  self.placement_state.set(None)
}

///|
/// 配置モードをキャンセル(要素を削除)
pub fn EditorState::cancel_placement(self : EditorState) -> Unit {
  guard self.placement_state.get() is Some(placement)

  // 配置中の要素を削除
  self.remove_element(placement.element_id)
  self.placement_state.set(None)
  self.select(None)
}

///|
/// 配置モード中かどうか
pub fn EditorState::is_placing(self : EditorState) -> Bool {
  self.placement_state.get() is Some(_)
}

// ============================================================
// バリデーション
// ============================================================

///|
/// 全要素を再バリデーションして結果を保存
pub fn EditorState::revalidate(self : EditorState) -> Unit {
  let elements = self.elements.get()
  let result = @model.validate_elements(elements)
  // プレーンSVG要素の権限を上書き
  let plain_ids = self.plain_svg_ids.get()
  for id in plain_ids {
    result.capabilities[id] = @model.ElementCapability::move_and_delete()
  }
  self.validation_result.set(Some(result))
}

///|
/// 指定要素の操作権限を取得
pub fn EditorState::get_element_capability(
  self : EditorState,
  element_id : String,
) -> @model.ElementCapability {
  // プレーンSVG要素は常に move_and_delete
  if self.plain_svg_ids.get().contains(element_id) {
    return @model.ElementCapability::move_and_delete()
  }
  // バリデーション結果から取得
  match self.validation_result.get() {
    Some(result) =>
      result.capabilities
      .get(element_id)
      .unwrap_or(@model.ElementCapability::full())
    None =>
      // バリデーション未実行時はフル権限
      @model.ElementCapability::full()
  }
}

///|
/// 指定要素がリサイズ可能かどうか
pub fn EditorState::can_resize_element(
  self : EditorState,
  element_id : String,
) -> Bool {
  self.get_element_capability(element_id).can_resize
}

///|
/// 指定要素が編集可能かどうか
pub fn EditorState::can_edit_element(
  self : EditorState,
  element_id : String,
) -> Bool {
  self.get_element_capability(element_id).can_edit
}

///|
/// プレーンSVG要素を登録
pub fn EditorState::register_plain_svg_ids(
  self : EditorState,
  ids : Array[String],
) -> Unit {
  let current = self.plain_svg_ids.get()
  let new_ids = current.copy()
  for id in ids {
    if not(new_ids.contains(id)) {
      new_ids.push(id)
    }
  }
  self.plain_svg_ids.set(new_ids)
}

///|
/// プレーンSVG要素をクリア
pub fn EditorState::clear_plain_svg_ids(self : EditorState) -> Unit {
  self.plain_svg_ids.set([])
}