diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 3160ee8..0658067 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -3968,7 +3968,7 @@ async function editCanvasObject(item, point = null) { if (!isForegroundDocument(tab) || openSeq !== state.openSeq) return; let rich = null; if (item.richText) rich = await loadRichText(item, face, tab, data.bytes); - else if (!item.scoped && layoutKnown) { + else if (layoutKnown) { try { rich = await loadRichText(item, face, tab, data.bytes); } catch {} } if (!isForegroundDocument(tab) || openSeq !== state.openSeq) return; @@ -4284,7 +4284,6 @@ async function toggleTextTool() { const font = state.textFonts[Number(fontPicker.value)]; state.textDefaults = { type: "TextObject", size: style.size, color: style.color, wrap: style.wrap, bold: Boolean(style.bold), italic: Boolean(style.italic), underline: Boolean(style.underline), strikeout: Boolean(style.strikeout), shadow: Boolean(style.shadow), background: style.background || "", - wrapOverride: style === state.textDefaults ? style.wrapOverride : style.wrap, align: style.align || "left", paragraphHeight: style.paragraphHeight || 0, letterSpacing: style.letterSpacing || 0, leftIndent: style.leftIndent || 0, rightIndent: style.rightIndent || 0, firstLineIndent: style.firstLineIndent || 0, fontChoice: font && !font.disabled ? font : state.textDefaults.fontChoice }; @@ -4312,7 +4311,7 @@ async function beginCanvasText(index, box, surface, page) { await face.load(); if (!isForegroundDocument(tab) || openSeq !== state.openSeq || !surface.isConnected) return; const node = document.createElement("div"); - const item = { ...state.textDefaults, ...box, wrap: state.textDefaults.wrapOverride ?? box.wrap ?? state.textDefaults.wrap, + const item = { ...state.textDefaults, ...box, wrap: Boolean(box.wrap), index, page, surface, node, text: "", draft: true, fontData: data }; const model = { ...richTextModel(item), autoWidth: !box.wrap }; const key = `temp-font-${crypto.randomUUID()}`; @@ -4578,7 +4577,6 @@ async function changeTextAlignment() { async function changeParagraph(wrap) { const item = currentTextStyle(); - const explicitWrap = wrap !== undefined; wrap ??= item.wrap; if (document.body.hasAttribute("data-busy") || item.draft) return; if (!el.textLineHeight.checkValidity()) { @@ -4593,7 +4591,6 @@ async function changeParagraph(wrap) { const spacing = inputMillimeters(el.textSpacing, item.letterSpacing || 0); if (item === state.textDefaults) { Object.assign(item, { wrap: Boolean(wrap), align: el.textAlign.value, paragraphHeight: height, letterSpacing: spacing }); - if (explicitWrap) item.wrapOverride = Boolean(wrap); updateObjectControls(canvasEditor.selected, true); return; } @@ -12490,12 +12487,13 @@ function updateObjectControls(item, reset = false) { el.undoButton.disabled = !state.editorInfo?.canUndo || !state.ready || state.exporting; el.redoButton.disabled = !state.editorInfo?.canRedo || !state.ready || state.exporting; } - el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input); + el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input); + el.textWrap.disabled = el.textLineHeight.disabled || text === state.textDefaults; const reflow = document.querySelector("#textReflow"), positioned = canvasEditor.input?.source && (item?.selectionIDs || !canEditObject(item, "layoutKnown")); reflow.hidden = !positioned; el.textWrap.hidden = !!positioned; reflow.disabled = textDisabled || !canEditObject(item, "reflow") || !!canvasEditor.input?.reflow; - el.paragraphButton.disabled = el.textWrap.disabled; + el.paragraphButton.disabled = el.textLineHeight.disabled; el.textAlign.disabled = el.textSize.disabled || Boolean(item?.items || item?.selectionIDs); const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown"); const rich = canvasEditor.input?.rich; diff --git a/assets/webui/ofdgo_edit.js b/assets/webui/ofdgo_edit.js index d286e1c..7f219b1 100644 --- a/assets/webui/ofdgo_edit.js +++ b/assets/webui/ofdgo_edit.js @@ -11,6 +11,10 @@ function sourceTextScale(editing) { return Math.max(Math.hypot(a, b), Math.hypot(c, d)) || 1; } +function textFrameBox(item) { + return { x: 0, y: 0, width: item.paragraphWidth ?? item.textFrame.width, height: item.textFrame.height }; +} + export function fontFaceStyle(data) { const style = { weight: "400", style: "normal" }; const bytes = ArrayBuffer.isView(data) ? new DataView(data.buffer, data.byteOffset, data.byteLength) : new DataView(data); @@ -936,8 +940,8 @@ export class CanvasEditor { }); if (item.contour) item.contour.svg.setAttribute("viewBox", `${item.x} ${item.y} ${item.width} ${item.height}`); if (item.textFrame) { - const { matrix: [a, b, c, d, e, f], width, height } = item.textFrame; - const box = item.textFrame.content || { x: 0, y: 0, width, height }; + const [a, b, c, d, e, f] = item.textFrame.matrix; + const box = textFrameBox(item); for (const handle of item.node.children) { if (handle.dataset.corner !== "w" && handle.dataset.corner !== "e") continue; const x = box.x + (handle.dataset.corner === "e" ? box.width : 0), y = box.y + box.height / 2; @@ -1265,7 +1269,7 @@ export class CanvasEditor { if (drag.item.textFrame) { const frame = drag.item.textFrame, [a, b, c, d] = frame.matrix; const dx = (d * (to.x - from.x) - c * (to.y - from.y)) / (a * d - b * c); - const box = reshapeBox({ geometry: frame.content || { x: 0, y: 0, width: frame.width, height: frame.height } }, dx, 0, drag.corner, false); + const box = reshapeBox({ geometry: textFrameBox(drag.item) }, dx, 0, drag.corner, false); drag.box = { x: drag.item.x + box.x, width: box.width }; const bounds = transformedBox(box, frame.matrix); Object.assign(drag.item.node.style, { left: `${bounds.x * PX_PER_MM}px`, top: `${bounds.y * PX_PER_MM}px`, @@ -1429,7 +1433,7 @@ export class CanvasEditor { this.drag = null; this.viewer.releasePointerCapture(drag.pointerID); const frame = drag.item.textFrame; - const geometry = drag.item.oriented?.box || drag.item.geometry || (frame ? { x: drag.item.x + (frame.content?.x || 0), width: frame.content?.width ?? frame.width } : drag.item); + const geometry = drag.item.oriented?.box || drag.item.geometry || (frame ? { x: drag.item.x, width: textFrameBox(drag.item).width } : drag.item); const changed = Object.keys(drag.box).some((key) => drag.box[key] !== geometry[key]); const apply = drag.item.shape || drag.item.oriented ? this.options.onReshape : canEditObject(drag.item, "stretch") ? this.options.onResize : this.options.onTextWidth; Promise.resolve(changed && apply(drag.item, drag.box)).finally(() => { @@ -1779,9 +1783,9 @@ export class CanvasEditor { else paragraph.append(document.createElement("br")); input.append(paragraph); } - const left = item.leftIndent || 0; - const frame = { ...(item.textFrame || { width: item.width, height: item.height, matrix: [1, 0, 0, 1, item.x, item.y] }) }; - if (item.wrap && item.paragraphWidth != null) frame.width = item.paragraphWidth; + const left = rich?.model.layout.leftIndent || item.leftIndent || 0; + const frame = { ...(rich?.frame || item.textFrame || { width: item.width, height: item.height, matrix: [1, 0, 0, 1, item.x, item.y] }) }; + if (!rich?.frame && item.paragraphWidth != null) frame.width = item.paragraphWidth; const [a, b, c, d, e, f] = frame.matrix; Object.assign(input.style, { left: "0px", top: "0px", diff --git a/internal/webui/webui.go b/internal/webui/webui.go index adf24c1..a2c3bfd 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -3401,7 +3401,7 @@ func (e *businessEngine) editorObjects(index int, text *ofdgo.PageText) ([]any, inverse, _ := matrix.Invert() local := inverse.TransformBox(box) width := math.Max(object.TextObject.Size, local.X+local.W) - if layout.Wrap { + if layout != (ofdgo.TextLayout{}) { item["paragraphWidth"] = frame.W } frame = ofdgo.Box{W: width, H: math.Max(object.TextObject.Size, local.Y+local.H)} @@ -4017,7 +4017,7 @@ func (e *businessEngine) compositeObjects(args []platformValue) (any, error) { inverse, _ := member.Matrix.Invert() local := inverse.TransformBox(box) width := math.Max(object.Size, local.X+local.W) - if layout.Wrap { + if layout != (ofdgo.TextLayout{}) { frame, err := object.TextFrame() if err != nil { return nil, err @@ -4969,8 +4969,12 @@ func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.Graphi if err != nil { return } - frame := matrix.TransformBox(contentBounds) format, layout := value.Runs[0].Format, value.Layout + selection := contentBounds + if !value.AutoWidth || layout.Wrap { + selection = ofdgo.Box{W: width, H: height} + } + frame := matrix.TransformBox(selection) var content strings.Builder for _, run := range value.Runs { content.WriteString(run.Text) @@ -4987,7 +4991,7 @@ func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.Graphi item["letterSpacing"], item["leftIndent"], item["rightIndent"], item["firstLineIndent"] = layout.LetterSpacing, layout.LeftIndent, layout.RightIndent, layout.FirstLineIndent item["paragraphWidth"], item["bounds"] = width, e.editorBox(frame) item["x"], item["y"], item["width"], item["height"] = frame.X, frame.Y, frame.W, frame.H - item["textFrame"] = map[string]any{"width": width, "height": height, "matrix": e.editorMatrix(matrix), "content": e.editorBox(contentBounds)} + item["textFrame"] = map[string]any{"width": width, "height": height, "matrix": e.editorMatrix(matrix)} capabilities := item["capabilities"].(map[string]any) for _, key := range []string{"textContent", "rewriteText", "replaceFont", "reflow", "layoutKnown", "paint"} { capabilities[key] = true diff --git a/ofdgo_editor_rich_text.go b/ofdgo_editor_rich_text.go index 02ec1b4..0c8db4b 100644 --- a/ofdgo_editor_rich_text.go +++ b/ofdgo_editor_rich_text.go @@ -743,7 +743,7 @@ func (e *Editor) promoteTextValue(t TextObject) (RichText, error) { } color = fmt.Sprintf("%d %d %d", r>>8, g>>8, b>>8) } - result := RichText{Runs: []RichTextRun{{Text: value, Format: TextFormat{Font: t.Font, Size: t.Size, Color: color, Bold: t.Weight == 700, Italic: t.Italic}}}, Layout: layout} + result := RichText{AutoWidth: t.layout == nil, Runs: []RichTextRun{{Text: value, Format: TextFormat{Font: t.Font, Size: t.Size, Color: color, Bold: t.Weight == 700, Italic: t.Italic}}}, Layout: layout} return result, result.Validate() } @@ -1134,7 +1134,8 @@ func (e *Editor) TargetRichText(page int, path ObjectPath) (RichText, error) { return value, err } -// PrepareRichText 读取独立富文本模型和局部编辑框,显式重排时保留原首行起点及页面变换 +// PrepareRichText 读取独立富文本模型和局部编辑框,定位文字采用自然宽度,已知段落保留宽度 +// 显式重排保留原首行起点及页面变换,不从原始Boundary推断段落宽度 // 入参: page 页面索引, path 完整对象路径, reflow 是否允许重排普通文字 // 返回: RichText 内容, Box 局部编辑框, Matrix 页面变换, error 路径、外观或排版错误 func (e *Editor) PrepareRichText(page int, path ObjectPath, reflow bool) (RichText, Box, Matrix, error) { @@ -1193,6 +1194,7 @@ func (e *Editor) PrepareRichText(page int, path ObjectPath, reflow bool) (RichTe if err != nil { return RichText{}, Box{}, Matrix{}, err } + autoWidth := text.layout == nil if reflow { value, layout := text.TextLayout() layout.AutoBidi = true @@ -1201,7 +1203,18 @@ func (e *Editor) PrepareRichText(page int, path ObjectPath, reflow bool) (RichTe } } value, err := e.promoteTextValue(text) - return value, Box{W: frame.W, H: frame.H}, matrix.Multiply(TranslationMatrix(dx, dy)), err + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + value.AutoWidth = autoWidth + if autoWidth { + result, err := e.layoutRichText(value, frame.W) + if err != nil { + return RichText{}, Box{}, Matrix{}, err + } + frame.W, frame.H = result.width, result.bounds.Y+result.bounds.H + } + return value, Box{W: frame.W, H: frame.H}, matrix.Multiply(TranslationMatrix(dx, dy)), nil } // ReflowRichText 原子重排完整路径文字,保留局部宽度、页面变换及共享实例隔离