Post-replace relayout · Decoration attachment

장식이 따라오지 못한다
두 가지 형태

두 실패는 Item 유형의 부재보다 “누구에게, 어느 기준점으로 붙어 있는가”가 sheetJson에 저장되지 않은 데서 시작합니다. 이 페이지는 Before 증거, 실제 이동, 권장 연결, 세 저장소의 역할을 한곳에 모았습니다.

Case 5628689 · 컨테이너 장식Case 5636021 · 텍스트 장식Live 편집 가능원본 파일 수정 없음

Executive summary

먼저 확인된 사실

Before sheetJson 기준으로 두 문서 모두 루트 GroupItem 아래가 평평합니다. 별도 장식 노드는 있지만 DecorationItem이나 소유 관계는 없습니다.

01 · STORED TYPE실제 유형은 그대로 유지

비트맵은 BitmapItem, 화살표는 VectorGraphicItem, 점은 ExtendableShapeItem입니다. 렌더러의 nodeType을 새 유형으로 바꿀 이유는 없습니다.

02 · MISSING LINK빠진 것은 소유와 기준점

role: Decoration만으로는 누구를 따라갈지 모릅니다. ownerNodeId, 양쪽 anchor, offset이 함께 있어야 합니다.

03 · BOUNDING BOX박스 합치기와 이동은 별개

Visual Bounding Box가 화살표까지 포함하면 점유·충돌 판단은 좋아집니다. 그래도 연결 정보가 없으면 화살표 자체는 움직이지 않습니다.

CASE 01 · 5628689

컨테이너에 붙은 장식

카드 밖으로 걸친 비트맵이 95% 포함 조건을 통과하지 못해 같은 이동 단위에서 빠집니다. 별도 요소로 남은 뒤 겹침 해소에서 카드와 반대 방향으로 밀렸습니다.

FAIL · 반대 방향 이동

Before 이미지와 노드 위치

Before · 저장된 실제 좌표
왜 연결에서 빠졌나

확성기와 알람시계의 카드 포함률은 각각 약 62.4%, 67.6%입니다. 현재 비텍스트 요소 연결 기준 95%에 미달합니다.

필요한 관계

확성기 → 카드 2, 알람시계 → 카드 3를 오른쪽·위 기준점에 연결해야 카드의 위치와 크기 변화에 함께 반응합니다.

소유 노드 저장 이동장식 저장 이동상대 간격 변화판정
카드 2 ↔ 확성기−112.89px+112.89px+225.78px반대 방향
카드 3 ↔ 알람시계−115.34px+115.34px+230.69px반대 방향

Live 연결 편집기

소유 노드와 기준점을 바꾸면 제안 위치와 sheetJson 조각이 즉시 갱신됩니다. 이 페이지 안에서만 바뀝니다.

LIVE · 편집 가능
소유 노드Before 장식저장된 After연결 적용 제안

제안 sheetJson 조각

CASE 02 · 5636021

텍스트에 붙은 장식

본문이 4줄에서 1줄로 줄고 폭은 259.65px 넓어졌지만, 화살표와 노란 점은 별도 형제 노드라 1px도 움직이지 않았습니다.

FAIL · 연결 없음

Before 이미지와 노드 위치

Before · 저장된 실제 좌표
현재 Visual Bounding Box 범위

실제 글자 영역과 내부 글머리 기호 공간만 포함합니다. 별도 VectorGraphicItem인 화살표는 박스 계산에도, 이동에도 들어오지 않습니다.

이미지에서 읽히는 소유 관계

화살표 → 위쪽 텍스트, 노란 점 → 31%로 보입니다. 다만 이는 시각 증거에 따른 제안이며 sheetJson에 저장된 사실은 아닙니다.

노드Before → After폭 변화높이 변화판정
위쪽 텍스트y +59.64px+259.65px−177.40px4줄 → 1줄
화살표x 0 / y 000허공에 남음
31% · 노란 점x 0 / y 000현재 결과와 일치 가능

Live 연결 편집기

화살표와 점을 각각 다른 텍스트에 연결해 볼 수 있습니다. Visual Bounding Box 포함 여부도 제안 JSON에 반영됩니다.

LIVE · 편집 가능
소유 노드Before 장식저장된 After연결 적용 제안

제안 sheetJson 조각

Three box models

Visual Bounding Box에 넣으면 무엇이 달라지나

점유 영역을 넓히는 일과 실제 노드를 움직이는 일을 분리해서 설계해야 합니다.

Text frame

편집 프레임 전체입니다. 줄바꿈 전후 폭과 높이를 담지만 실제 글자나 바깥 장식의 시각 점유를 정확히 나타내지 않습니다.

현재 이동의 입력 박스

Visual union

글자의 tight box와 연결된 장식 박스를 합칩니다. 충돌·여백 검증은 좋아지지만 장식 좌표를 자동으로 바꾸지는 않습니다.

판정에는 유효 · 이동은 부족

Attachment transform

소유 노드의 anchor가 바뀔 때 장식의 anchor와 offset으로 새 좌표를 계산합니다. 그 결과를 Visual union에 다시 반영합니다.

권장 · 이동과 점유를 함께 해결

A같은 노드, 세 개의 상자

크기가 다른 세 상자가 아니라 서로 다른 질문에 답하는 세 개의 자입니다. 프레임은 편집기가 저장한 칸, tight bounding box는 글자가 실제로 찍힌 자리, visual bounding box는 그 글자에 딸린 장식까지 덮는 자리입니다. 아래는 케이스 5628689의 두 텍스트 노드를 실측 좌표 그대로 겹쳐 그린 것입니다.

한 텍스트 노드의 세 상자 — 프레임 · tight · visual 글머리 기호가 있는 텍스트에서는 tight bounding box가 기호를 지나친 자리에서 시작하고 visual bounding box가 그 들여쓰기 띠까지 왼쪽으로 넓어진다. 기호가 없는 텍스트에서는 두 상자가 같다. 불릿 2-1 · listStyle: unordered — 기호가 있다 tight는 기호를 지나친 자리에서 시작한다. visual이 그 띠를 되찾는다. 침실에는 기기를 두지 않는다 들여쓰기 띠 40.44px = 1.05em — 기호가 쓰라고 비워 둔 자리 프레임 boundingBox 548.90 × 53.79 tight bounding box 409.19 × 32.86 left +40.44 visual bounding box 449.63 × 32.86 left 0 환경 재설계 · listStyle 없음 — 기호가 없다 넓힐 띠가 없다. visual = tight, 값이 그대로 나온다. 환경 재설계 1.19px — 기호 몫이 아니다 (글자 자체의 여백) 프레임 boundingBox 520.29 × 53.79 tight bounding box 165.45 × 32.75 left +1.19 visual bounding box 165.45 × 32.75 tight와 동일
프레임 boundingBox — 문서에 저장된 칸 tight bounding box — 본문 글자의 glyph extent visual bounding box — 판정에 쓰는 상자 글머리 기호 — 본문과 다른 layer에 그려진다
tight bounding box는 문서 JSON에 없습니다. 엔진의 TextBoundingBoxCalculator.calculateTightBoundingBox만이 값을 정의하고, src/text-tight-box/가 브라우저·폰트를 들고 재현해 구워 둔 것입니다. 글머리 기호는 렌더러가 본문과 같은 layer에 따로 그리므로(CanvasTextDrawer의 「5. markers」) 그 상자에 들어오지 않습니다.
상자무엇을 재나불릿 2-1 (기호 있음)환경 재설계 (기호 없음)쓰는 곳
프레임 boundingBox편집 프레임 — 오토핏 여백까지 포함548.90 × 53.79520.29 × 53.79저장 · 실제 이동(translateNode)
tight bounding box본문 글자의 glyph extent. 기호 제외409.19 × 32.86 left +40.44165.45 × 32.75 left +1.19넘침 · 겹침의 원자료
visual bounding boxtight + 기호가 쓰라고 비워 둔 들여쓰기 띠449.63 × 32.86 left 0165.45 × 32.75 tight와 동일채점 · 게이트 · 이동 후보의 간격
왜 tight만으로는 안 되나

기호가 있으면 tight가 기호를 지나친 자리에서 시작합니다. 커밋된 코퍼스 실측으로 tight의 left(프레임 원점 기준) 중앙값이 기호 있는 텍스트 117건은 30.94px, 없는 텍스트 789건은 3.78px였습니다. 그 상자로 재면 「기호는 도형 밖으로 나가 있는데 넘침 0」이 나옵니다.

visual이 넓히는 방향은 왼쪽 하나뿐

visualBoxMap은 왼쪽 변만 프레임 안쪽 끝까지 밀고 오른쪽·위·아래는 tight 그대로 둡니다 — 그래서 visual은 언제나 tight의 superset입니다. 넓히는 양은 MARKER_INDENT_MAX_EM = 2.5로 묶여 있습니다. 가운데·오른쪽 정렬에서 정렬 여백만큼 상자가 터지는 것을 막는 상한이고, 왼쪽 정렬(코퍼스의 96%)은 여기 걸리지 않습니다. 위 불릿은 40.44px = 1.05em이라 상한 96.06px에 닿지 않습니다.

여기서 반드시 짚어야 하는 사실

오늘의 visual bounding box는 바깥 장식 노드를 포함하지 않습니다. 텍스트 노드 하나의 상자를 글머리 기호 몫만큼 왼쪽으로 넓힐 뿐이고, 별도 VectorGraphicItem·BitmapItem은 그 지도에 아예 들어오지 않습니다. 아래 카드 ②의 「Visual union」은 현재 구현이 아니라 제안입니다.

B장식을 그 상자에 넣으면 무슨 일이 생기나

케이스 5636021의 화살표를 여기까지 여세요에 연결했다고 보고, visual bounding box를 tight ∪ 장식으로 넓혀 본 것입니다. 좌표는 모두 저장된 Before/After에서 읽은 실측값이고, ③의 offset만 Before 배치에서 역산한 파생값입니다.

장식을 visual bounding box에 넣었을 때 — 케이스 5636021 합집합만 넣으면 판정 상자는 화살표를 덮도록 커지지만 화살표 자체는 움직이지 않는다. anchor와 offset이 있어야 화살표가 새 글자 끝으로 따라간다. ① Before · 저장된 상태 화살표는 글자 끝 오른쪽 29.61px에 놓여 있다 여기 까지 여세 화살표 — 글자 끝에서 +29.61 visual union 196.86 × 221.77 tight ∪ 화살표 → 폭 100.37 → 196.86 판정용으로는 이 편이 정직하다 (+96.49px) ② After · 현재 저장된 결과 글자는 4줄에서 1줄로 내려앉고 넓어졌다 Before tight 여기까지 여세요 y +58.73 화살표 Δ 0 — 제자리 +48.68 visual union 369.79 × 100.02 상자는 위로 48.68px 자랐다 (51.34 → 100.02) 화살표는 한 픽셀도 움직이지 않았다 — 허공에 남는다 ③ 제안 · 연결이 있을 때 anchor + offset(+29.61, +10.05)으로 새 좌표를 먼저 계산한다 여기까지 여세요 옛 자리 새 자리 — 다시 글자 끝 +29.61 visual union 466.29 × 51.34 화살표 이동 +263.75 · +58.73 → 다시 글자 끝 높이는 51.34 그대로 — 헛된 세로 팽창이 사라진다
프레임 boundingBox tight bounding box visual union — tight ∪ 장식 장식 노드 b099744c · VectorGraphicItem 66.88 × 33.59
①→② 사이에서 텍스트는 4줄에서 1줄로 내려앉으며 y +59.64 · 폭 +259.65 · 높이 −177.40만큼 바뀌었고, 화살표의 deltaleft 0 / top 0이었습니다. ②의 union이 위로 자란 48.68px은 「글자가 거기 있다」는 뜻이 아니라 화살표가 아직 옛 자리에 있다는 뜻입니다.
상태텍스트 tight화살표visual union화살표 이동
① Before100.37 × 221.7766.88 × 33.59196.86 × 221.77 폭 +96.49
② After · 현재369.79 × 51.3466.88 × 33.59 제자리369.79 × 100.02 높이 +48.680px
③ 연결 적용 제안369.79 × 51.3466.88 × 33.59466.29 × 51.34 높이 그대로+263.75 · +58.73
좋아지는 것 — 판정이 정직해진다

화살표가 점유 영역으로 세어집니다. 지금은 문서 경계를 넘든 옆 카드를 밟든 지표가 0을 보고합니다. 합집합을 쓰면 Before 기준 폭이 100.37에서 196.86으로 +96.49px — 눈에 보이는 것과 표가 같아집니다.

그래도 안 되는 것 — 상자는 노드를 끌고 가지 않는다

②가 그 증거입니다. union은 위로 48.68px 자랐지만 화살표의 좌표는 그대로입니다. 상자를 넓히는 일은 점유의 서술이고, 화살표를 옮기는 일은 좌표의 변경이라 서로 다른 층입니다. 연결 정보가 없으면 두 번째는 일어나지 않습니다.

그래서 「박스만 먼저 넓히고 이동은 나중에」가 안 된다

visual bounding box는 채점 전용이 아닙니다. placementBoxMap이 이 상자를 그대로 이동 후보에게 넘겨(escapeIntoHorizontalSlack · spreadIntoVerticalSlack) 간격과 장애물 여유를 재는 자로 씁니다. 안 움직일 노드를 상자에 넣는 순간 그만큼 이동량이 바뀝니다 — ②의 상자로 위쪽 여백을 재면 48.68px을 없는 여백으로 착각합니다. placementBox.ts가 「위상 동결·translateNode에는 이 지도가 아니라 실제 프레임 absBoxMap을 써야 한다」고 못 박아 둔 이유가 이것입니다. 판정 상자와 이동 상자는 끝까지 갈라 두고, 장식은 먼저 옮긴 뒤에 합쳐야 합니다 — ③의 순서입니다.

5636021에 적용할 순서

① 화살표를 위쪽 TextItem에 연결 → ② 텍스트 After box에서 화살표 위치 계산 → ③ 이동된 화살표와 글자의 tight box를 합쳐 Visual Bounding Box 생성 → ④ 합쳐진 영역으로 충돌과 문서 경계를 검증합니다. 노란 점은 31%에 별도 연결해야 합니다.

Cross-repository contract

세 저장소를 잇는 공통 계약

의미 판정은 aippt-prisonbreak, 저장과 편집 보존은 miricanvas-web-2, 재배치와 검증은 miricanvas-iui가 맡는 구조가 가장 작고 안전합니다.

01 · AUTHOR

aippt-prisonbreak

RLSC에서 Role.Element.Decoration을 판정하고 소유 후보를 확인합니다.

02 · CONTRACT

sheetJson props

기존 nodeType을 유지한 채 rolelayoutAttachment를 저장합니다.

03 · CONSUME

iui + web-2

iui는 이동·점유·충돌을 검증하고, web-2는 저장·편집·복제에서 관계를 보존합니다.

유형을 새로 만들지 않는 이유

  • BitmapItem, VectorGraphicItem 등 실제 렌더 유형을 그대로 사용할 수 있습니다.
  • TextDecorationItem, ContainerDecorationItem은 UI 표시명이나 TypeScript 별칭으로 제공할 수 있습니다.
  • 이를 실제 nodeType으로 만들면 엔진 등록, 역직렬화, 편집기 도구, 복제 경로까지 영향 범위가 커집니다.
  • Role.LayoutContainer.Decoration은 “그 컨테이너 자체가 장식”이라는 뜻이라 소유 관계를 대체하지 않습니다.

권장 최소 형태

{
  "nodeType": "BitmapItem",
  "props": {
    "role": "Decoration",
    "layoutAttachment": {
      "kind": "container",
      "ownerNodeId": "d63bbcb1-…",
      "ownerAnchor": "top-right",
      "selfAnchor": "top-right",
      "offset": { "x": 65.19, "y": -51.43 },
      "sizePolicy": "keep",
      "includeInVisualBounds": true
    }
  }
}
aippt-prisonbreak장식 역할을 판정하고 owner 후보와 attachment 의미를 생성합니다. RLSC와 sheetJson이 별도라면 변환 단계에서 node id 대응을 보존해야 합니다.
miricanvas-web-2NodeProps.role에 이미 있는 저장 통로를 확장해 layoutAttachment를 직렬화·역직렬화하고, 복제·그룹 이동·편집에서 owner id를 보정합니다.
miricanvas-iuiattachment transform을 재배치 전에 고정하고, 소유 노드의 새 anchor에서 장식 좌표를 계산한 뒤 Visual Bounding Box와 충돌 지표를 다시 계산합니다.

권장 명명

저장 계약은 role: Decoration + layoutAttachment.kind: text | container로 두고, 화면에는 이를 조합해 TextDecorationItem 또는 ContainerDecorationItem으로 보여주는 방식이 좋습니다. 의미는 명확해지고 기존 Item 처리 경로는 유지됩니다.

Evidence & source

확인한 파일과 데이터

이 페이지의 Before/After 좌표는 위 JSON에서 읽은 값입니다. 케이스 5636021의 “화살표→위쪽 텍스트, 점→31%” 관계만 시각 증거에 따른 제안이며 원본 sheetJson에는 없습니다.