From 08ca0c99b227aea074658c7f191585cce7bf3859 Mon Sep 17 00:00:00 2001 From: Ron Carbot Date: Wed, 2 Sep 2026 17:07:01 +0000 Subject: [PATCH 1/7] Implement Graphics.drawGraphicsData and readGraphicsData. Walk IGraphicsData by data_type into the existing fill/stroke/path APIs, keep recorded paths after tessellation, and serialize MOVE_TO/LINE_TO/CURVE_TO plus fills, strokes, and GraphicsEndFill. --- index.ts | 1 + lib/Graphics.ts | 322 ++++++++++++++++++++++++++++++++++-- lib/draw/GraphicsEndFill.ts | 14 ++ 3 files changed, 321 insertions(+), 16 deletions(-) create mode 100644 lib/draw/GraphicsEndFill.ts diff --git a/index.ts b/index.ts index 5d20e17..9edf9fc 100644 --- a/index.ts +++ b/index.ts @@ -127,6 +127,7 @@ export { SolidFillStyle } from './lib/draw/fills/SolidFillStyle'; export { GraphicsFactoryFills } from './lib/draw/GraphicsFactoryFills'; export { GraphicsFactoryHelper } from './lib/draw/GraphicsFactoryHelper'; export { GraphicsFactoryStrokes } from './lib/draw/GraphicsFactoryStrokes'; +export { GraphicsEndFill } from './lib/draw/GraphicsEndFill'; export { GraphicsFillStyle } from './lib/draw/GraphicsFillStyle'; export { GraphicsStrokeStyle } from './lib/draw/GraphicsStrokeStyle'; export { GraphicsPath } from './lib/draw/GraphicsPath'; diff --git a/lib/Graphics.ts b/lib/Graphics.ts index 0e30563..dd88ec1 100644 --- a/lib/Graphics.ts +++ b/lib/Graphics.ts @@ -37,9 +37,10 @@ import { BitmapFillStyle } from './draw/fills/BitmapFillStyle'; import { GradientFillStyle } from './draw/fills/GradientFillStyle'; import { SolidFillStyle } from './draw/fills/SolidFillStyle'; import { GraphicsPathWinding } from './draw/GraphicsPathWinding'; -import { IGraphicsData } from './draw/IGraphicsData'; +import { IFillStyle, IGraphicsData } from './draw/IGraphicsData'; import { GraphicsStrokeStyle } from './draw/GraphicsStrokeStyle'; import { GraphicsFillStyle } from './draw/GraphicsFillStyle'; +import { GraphicsEndFill } from './draw/GraphicsEndFill'; import { Shape } from './renderables/Shape'; import { SegmentedPath } from './data/SegmentedPath'; import { FillType } from './data/FillType'; @@ -126,6 +127,8 @@ export class Graphics extends AssetBase { private _queued_fill_pathes: GraphicsPath[] = []; private _queued_stroke_pathes: GraphicsPath[] = []; + private _recorded_fill_pathes: GraphicsPath[] = []; + private _recorded_stroke_pathes: GraphicsPath[] = []; public _active_fill_path: GraphicsPath; public _active_stroke_path: GraphicsPath; private _lineStyle: GraphicsStrokeStyle; @@ -219,8 +222,10 @@ export class Graphics extends AssetBase { if (!isLine) { this._drawingDirty = true; this._queued_fill_pathes.push(value); + this._recordFillPath(value); } else { this._queued_stroke_pathes.push(value); + this._recordStrokePath(value); if (!supressFill) { this.endFill(); @@ -277,6 +282,10 @@ export class Graphics extends AssetBase { this._owners.remove(owner); } + public forEachOwner(callback: (owner: IContainer) => void): void { + this._owners.forEach(callback); + } + public invalidate(): void { super.invalidate(); @@ -352,6 +361,8 @@ export class Graphics extends AssetBase { graphics.sourceGraphics = this; graphics._addShapes(this._shapes, cloneShapes); + graphics._recorded_fill_pathes = this._recorded_fill_pathes.concat(); + graphics._recorded_stroke_pathes = this._recorded_stroke_pathes.concat(); } public clone(cloneShapes: boolean = false): Graphics { @@ -446,6 +457,8 @@ export class Graphics extends AssetBase { this._active_stroke_path = null; this._queued_fill_pathes.length = 0; this._queued_stroke_pathes.length = 0; + this._recorded_fill_pathes.length = 0; + this._recorded_stroke_pathes.length = 0; this._current_position.x = 0; this._current_position.y = 0; this._drawingDirty = false; @@ -954,25 +967,60 @@ export class Graphics extends AssetBase { * */ public drawGraphicsData(graphicsData: Array): void { - /* - for (var i:number=0; i { + const result: IGraphicsData[] = []; + + const fillPaths = this._collectRecordedPaths( + this._recorded_fill_pathes, this._queued_fill_pathes, this._active_fill_path); + for (let i = 0; i < fillPaths.length; i++) { + const path = fillPaths[i]; + if (!path.commands || !path.commands.length) + continue; + + const fill = this._unwrapFill(path.style); + if (fill) + result.push(fill); + + result.push(this._clonePathForRead(path)); + result.push(new GraphicsEndFill()); + } - } - else if(graphicsData[i].dataType=="Path"){ + const strokePaths = this._collectRecordedPaths( + this._recorded_stroke_pathes, this._queued_stroke_pathes, this._active_stroke_path); + for (let i = 0; i < strokePaths.length; i++) { + const path = strokePaths[i]; + if (!path.commands || !path.commands.length) + continue; - } + const stroke = path.stroke; + if (stroke) + result.push(stroke); - } - */ + result.push(this._clonePathForRead(path)); + } + return result; } /** @@ -1038,7 +1086,12 @@ export class Graphics extends AssetBase { this.invalidate(); } - private _drawPathInternal(path: GraphicsPath, commands: Int32Array, data: Float64Array, winding: GraphicsPathWinding) { + private _drawPathInternal( + path: GraphicsPath, + commands: Int32Array | ArrayLike, + data: Float64Array | ArrayLike, + winding: GraphicsPathWinding + ) { let dataPosition = 0; for (let i = 0; i < commands.length; i++) { switch (commands[i]) { @@ -1051,9 +1104,26 @@ export class Graphics extends AssetBase { dataPosition += 2; break; case GraphicsPathCommand.CURVE_TO: - path.curveTo(data[dataPosition], data[dataPosition + 1],data[dataPosition + 2], data[dataPosition + 3]); + path.curveTo(data[dataPosition], data[dataPosition + 1], data[dataPosition + 2], data[dataPosition + 3]); dataPosition += 4; break; + case GraphicsPathCommand.CUBIC_CURVE: + path.cubicCurveTo( + data[dataPosition], data[dataPosition + 1], + data[dataPosition + 2], data[dataPosition + 3], + data[dataPosition + 4], data[dataPosition + 5]); + dataPosition += 6; + break; + case GraphicsPathCommand.WIDE_MOVE_TO: + dataPosition += 2; + path.moveTo(data[dataPosition], data[dataPosition + 1]); + dataPosition += 2; + break; + case GraphicsPathCommand.WIDE_LINE_TO: + dataPosition += 2; + path.lineTo(data[dataPosition], data[dataPosition + 1]); + dataPosition += 2; + break; case GraphicsPathCommand.NO_OP: default: } @@ -1804,12 +1874,231 @@ export class Graphics extends AssetBase { return; } + + private _recordFillPath(path: GraphicsPath): void { + const recorded = this._recorded_fill_pathes; + if (recorded[recorded.length - 1] !== path) + recorded.push(path); + } + + private _recordStrokePath(path: GraphicsPath): void { + const recorded = this._recorded_stroke_pathes; + if (recorded[recorded.length - 1] !== path) + recorded.push(path); + } + + private _collectRecordedPaths( + recorded: GraphicsPath[], + queued: GraphicsPath[], + active: GraphicsPath + ): GraphicsPath[] { + const result: GraphicsPath[] = []; + const add = (list: GraphicsPath[]) => { + if (!list) + return; + for (let i = 0; i < list.length; i++) { + const path = list[i]; + if (!path || result.indexOf(path) != -1) + continue; + result.push(path); + } + }; + add(recorded); + add(queued); + if (active) + add([active]); + return result; + } + + private _unwrapFill(style: IGraphicsData): IFillStyle { + if (!style) + return null; + + if (style.data_type == GraphicsFillStyle.data_type) + return (> style).fillStyle; + + if (style.data_type == GraphicsStrokeStyle.data_type) + return this._unwrapFill((> style).fillStyle); + + return style; + } + + private _clonePathForRead(path: GraphicsPath): GraphicsPath { + const commands: GraphicsPathCommand[] = []; + const data: number[] = []; + const srcCommands = path.commands || []; + const srcData = path.data || []; + let d = 0; + + for (let i = 0; i < srcCommands.length; i++) { + const cmd = srcCommands[i]; + switch (cmd) { + case GraphicsPathCommand.MOVE_TO: + case GraphicsPathCommand.LINE_TO: + commands.push(cmd); + data.push(srcData[d++], srcData[d++]); + break; + case GraphicsPathCommand.CURVE_TO: + commands.push(cmd); + data.push(srcData[d++], srcData[d++], srcData[d++], srcData[d++]); + break; + case GraphicsPathCommand.CUBIC_CURVE: { + const c1x = srcData[d++]; + const c1y = srcData[d++]; + const c2x = srcData[d++]; + const c2y = srcData[d++]; + const ax = srcData[d++]; + const ay = srcData[d++]; + // AIR readGraphicsData only returns MOVE_TO, LINE_TO, CURVE_TO. + commands.push(GraphicsPathCommand.CURVE_TO); + data.push((c1x + c2x) * 0.5, (c1y + c2y) * 0.5, ax, ay); + break; + } + case GraphicsPathCommand.WIDE_MOVE_TO: + d += 2; + commands.push(GraphicsPathCommand.MOVE_TO); + data.push(srcData[d++], srcData[d++]); + break; + case GraphicsPathCommand.WIDE_LINE_TO: + d += 2; + commands.push(GraphicsPathCommand.LINE_TO); + data.push(srcData[d++], srcData[d++]); + break; + default: + break; + } + } + + return new GraphicsPath(commands, data, path.winding); + } + + private _drawGraphicsDataItem(item: IGraphicsData): void { + const type = item.data_type; + + if (type == GraphicsPath.data_type) { + const path = item; + this.drawPath( path.commands, path.data, path.winding); + return; + } + + if (type == GraphicsEndFill.data_type) { + this.endFill(); + return; + } + + if (type == GraphicsFillStyle.data_type) { + this._beginFillFromStyle((> item).fillStyle); + return; + } + + if (type == GraphicsStrokeStyle.data_type) { + this._lineStyleFromStroke(> item); + return; + } + + if (type == SolidFillStyle.data_type || + type == GradientFillStyle.data_type || + type == BitmapFillStyle.data_type) { + this._beginFillFromStyle( item); + } + } + + private _beginFillFromStyle(fill: IFillStyle): void { + if (!fill) + return; + + if (fill.data_type == GraphicsFillStyle.data_type) + fill = (> fill).fillStyle; + + if (fill.data_type == SolidFillStyle.data_type) { + const solid = fill; + this.beginFill(solid.color, solid.alpha); + return; + } + + if (fill.data_type == GradientFillStyle.data_type) { + const gradient = fill; + this.beginGradientFill( + gradient.type, + gradient.colors, + gradient.alphas, + gradient.ratios, + gradient.matrix || new Matrix(), + gradient.spreadMethod, + gradient.interpolationMethod, + gradient.focalPointRatio + ); + return; + } + + if (fill.data_type == BitmapFillStyle.data_type) { + const bitmap = fill; + this.beginBitmapFill( + bitmap.image, bitmap.matrix, bitmap.repeat, bitmap.smooth); + } + } + + private _lineStyleFromStroke(stroke: GraphicsStrokeStyle): void { + const fill = this._unwrapFill(stroke.fillStyle); + + if (fill && fill.data_type == SolidFillStyle.data_type) { + const solid = fill; + this.lineStyle( + stroke.thickness, + solid.color, + solid.alpha, + false, + stroke.scaleMode, + stroke.capstyle, + stroke.jointstyle, + stroke.miterLimit + ); + return; + } + + this.lineStyle( + stroke.thickness, + 0, + 1, + false, + stroke.scaleMode, + stroke.capstyle, + stroke.jointstyle, + stroke.miterLimit + ); + + if (!fill) + return; + + if (fill.data_type == GradientFillStyle.data_type) { + const gradient = fill; + this.lineGradientStyle( + gradient.type, + gradient.colors, + gradient.alphas, + gradient.ratios, + gradient.matrix || new Matrix(), + gradient.spreadMethod, + gradient.interpolationMethod, + gradient.focalPointRatio + ); + return; + } + + if (fill.data_type == BitmapFillStyle.data_type) { + const bitmap = fill; + this.lineBitmapStyle( + bitmap.image, bitmap.matrix, bitmap.repeat, bitmap.smooth); + } + } + private _updateFillPath() { if (this._fillStyle) { if (this._active_fill_path == null || this._active_fill_path.style != this._fillStyle) { this._active_fill_path = new GraphicsPath(); this._active_fill_path.style = this._fillStyle; this._queued_fill_pathes.push(this._active_fill_path); + this._recordFillPath(this._active_fill_path); //auto-add move command if starting position is not zero if (this._current_position.x != 0 || this._current_position.y != 0) @@ -1826,6 +2115,7 @@ export class Graphics extends AssetBase { this._active_stroke_path = new GraphicsPath(); this._active_stroke_path.style = this._lineStyle; this._queued_stroke_pathes.push(this._active_stroke_path); + this._recordStrokePath(this._active_stroke_path); //auto-add move command if starting position is not zero if (this._current_position.x != 0 || this._current_position.y != 0) diff --git a/lib/draw/GraphicsEndFill.ts b/lib/draw/GraphicsEndFill.ts new file mode 100644 index 0000000..ea6f423 --- /dev/null +++ b/lib/draw/GraphicsEndFill.ts @@ -0,0 +1,14 @@ +import { IGraphicsData } from './IGraphicsData'; + +/** + * Indicates the end of a graphics fill. Matching Adobe's GraphicsEndFill, + * this is submitted to Graphics.drawGraphicsData() and returned from + * Graphics.readGraphicsData(). + */ +export class GraphicsEndFill implements IGraphicsData { + public static readonly data_type: string = '[graphicsdata EndFill]'; + + public get data_type(): string { + return GraphicsEndFill.data_type; + } +} From a8ca577de0c06d01f2e413a5d591e768fbbe90d1 Mon Sep 17 00:00:00 2001 From: Ron Carbot Date: Wed, 2 Sep 2026 18:57:55 +0000 Subject: [PATCH 2/7] Fix author-time bitmap fills: per-image materials, NPOT mipmaps, and readGraphicsData reconstruction. --- lib/Graphics.ts | 215 +++++++++++++++++++++++++++++-- lib/draw/GraphicsFactoryFills.ts | 5 +- lib/flash/StyleUtils.ts | 11 +- lib/managers/MaterialManager.ts | 22 +++- 4 files changed, 238 insertions(+), 15 deletions(-) diff --git a/lib/Graphics.ts b/lib/Graphics.ts index dd88ec1..7e36caf 100644 --- a/lib/Graphics.ts +++ b/lib/Graphics.ts @@ -100,14 +100,16 @@ export class Graphics extends AssetBase { shapeStyle.smooth ); - const material = MaterialManager.getMaterialForBitmap(true); + const material = MaterialManager.getMaterialForBitmap(true, shapeStyle.image); - //enforce image smooth style - style.sampler = new ImageSampler(shapeStyle.repeat, shapeStyle.smooth, shapeStyle.smooth); + //enforce image smooth style (mipmap=false: 3rd sampler arg is mipmap) + style.sampler = new ImageSampler(shapeStyle.repeat, shapeStyle.smooth, false); style.uvMatrix = bitmapFillStyle.getUVMatrix(); - return Shape.getShape(element, material, style); + const shape = Shape.getShape(element, material, style); + shape.originalFillStyle = bitmapFillStyle; + return shape; } public static getGraphics(): Graphics { @@ -586,6 +588,11 @@ export class Graphics extends AssetBase { if (this._fillStyle) this.endFill(); + if (!bitmap) { + console.warn('[beginBitmapFill] null bitmap'); + return; + } + if (!this._bitmapFillPool) { this._bitmapFillPool = {}; } @@ -601,6 +608,7 @@ export class Graphics extends AssetBase { smooth) ); } else { + fill.fillStyle.image = bitmap; fill.fillStyle.matrix = matrix; fill.fillStyle.repeat = repeat; fill.fillStyle.smooth = smooth; @@ -990,27 +998,32 @@ export class Graphics extends AssetBase { */ public readGraphicsData(): Array { const result: IGraphicsData[] = []; + const emittedFills: IFillStyle[] = []; const fillPaths = this._collectRecordedPaths( this._recorded_fill_pathes, this._queued_fill_pathes, this._active_fill_path); for (let i = 0; i < fillPaths.length; i++) { - const path = fillPaths[i]; - if (!path.commands || !path.commands.length) + const path = this._ensurePathCommands(fillPaths[i]); + if (!this._pathHasDrawableCommands(path)) continue; const fill = this._unwrapFill(path.style); - if (fill) + if (fill) { result.push(fill); + emittedFills.push(fill); + } result.push(this._clonePathForRead(path)); result.push(new GraphicsEndFill()); } + this._appendBitmapFillsFromShapes(result, emittedFills); + const strokePaths = this._collectRecordedPaths( this._recorded_stroke_pathes, this._queued_stroke_pathes, this._active_stroke_path); for (let i = 0; i < strokePaths.length; i++) { - const path = strokePaths[i]; - if (!path.commands || !path.commands.length) + const path = this._ensurePathCommands(strokePaths[i]); + if (!this._pathHasDrawableCommands(path)) continue; const stroke = path.stroke; @@ -1493,6 +1506,9 @@ export class Graphics extends AssetBase { bitmap: BitmapImage2D, matrix: Matrix = null, repeat: boolean = true, smooth: boolean = false): void { + if (!bitmap) + return; + if (this._lineStyle) { this._lineStyle = this._lineStyle.clone(); this._lineStyle.fillStyle = new GraphicsFillStyle( @@ -1923,6 +1939,185 @@ export class Graphics extends AssetBase { return style; } + private _pathHasDrawableCommands(path: GraphicsPath): boolean { + const cmds = path && path.commands; + if (!cmds || !cmds.length) + return false; + + for (let i = 0; i < cmds.length; i++) { + const cmd = cmds[i]; + if (cmd == GraphicsPathCommand.LINE_TO || + cmd == GraphicsPathCommand.CURVE_TO || + cmd == GraphicsPathCommand.CUBIC_CURVE || + cmd == GraphicsPathCommand.WIDE_LINE_TO) + return true; + } + return false; + } + + private _ensurePathCommands(path: GraphicsPath): GraphicsPath { + if (!path || this._pathHasDrawableCommands(path)) + return path; + + const verts = path.verts; + if (!verts || verts.length < 6) + return path; + + const outline = this._outlineFromTriangleVerts(verts); + if (outline.length < 6) + return path; + + const commands: GraphicsPathCommand[] = [GraphicsPathCommand.MOVE_TO]; + const data: number[] = [outline[0], outline[1]]; + for (let i = 2; i + 1 < outline.length; i += 2) { + commands.push(GraphicsPathCommand.LINE_TO); + data.push(outline[i], outline[i + 1]); + } + + const clone = new GraphicsPath(commands, data, path.winding); + clone.style = path.style; + return clone; + } + + private _outlineFromTriangleVerts(verts: number[]): number[] { + if (!verts || verts.length < 6) + return []; + + const quant = (v: number) => Math.round(v * 1000) / 1000; + const edgeKey = (x1: number, y1: number, x2: number, y2: number) => { + if (x1 < x2 || (x1 === x2 && y1 < y2)) + return x1 + ',' + y1 + '>' + x2 + ',' + y2; + return x2 + ',' + y2 + '>' + x1 + ',' + y1; + }; + + const edges = new Map(); + for (let i = 0; i + 5 < verts.length; i += 6) { + const xs = [quant(verts[i]), quant(verts[i + 2]), quant(verts[i + 4])]; + const ys = [quant(verts[i + 1]), quant(verts[i + 3]), quant(verts[i + 5])]; + for (let e = 0; e < 3; e++) { + const ax = xs[e]; + const ay = ys[e]; + const bx = xs[(e + 1) % 3]; + const by = ys[(e + 1) % 3]; + const k = edgeKey(ax, ay, bx, by); + const rec = edges.get(k); + if (rec) + rec.n++; + else + edges.set(k, { ax, ay, bx, by, n: 1 }); + } + } + + const boundary: { ax: number, ay: number, bx: number, by: number }[] = []; + edges.forEach((rec) => { + if (rec.n === 1) + boundary.push(rec); + }); + + if (!boundary.length) + return []; + + const used: boolean[] = []; + for (let i = 0; i < boundary.length; i++) + used[i] = false; + + const out: number[] = [boundary[0].ax, boundary[0].ay, boundary[0].bx, boundary[0].by]; + used[0] = true; + let cx = boundary[0].bx; + let cy = boundary[0].by; + const eps = 0.001; + + for (let n = 1; n < boundary.length; n++) { + let found = -1; + for (let i = 0; i < boundary.length; i++) { + if (used[i]) + continue; + const e = boundary[i]; + if (Math.abs(e.ax - cx) < eps && Math.abs(e.ay - cy) < eps) { + found = i; + cx = e.bx; + cy = e.by; + break; + } + if (Math.abs(e.bx - cx) < eps && Math.abs(e.by - cy) < eps) { + found = i; + cx = e.ax; + cy = e.ay; + break; + } + } + if (found < 0) + break; + used[found] = true; + out.push(cx, cy); + } + + return out; + } + + private _appendBitmapFillsFromShapes(result: IGraphicsData[], emitted: IFillStyle[]): void { + const shapes = this._shapes; + if (!shapes) + return; + + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i]; + if (!shape || !shape.elements || shape.elements.assetType == LineElements.assetType) + continue; + + let fill = this._unwrapFill(shape.originalFillStyle); + if (!fill || fill.data_type != BitmapFillStyle.data_type) { + const image = shape.style && shape.style.image; + if (!image) + continue; + fill = new BitmapFillStyle( image, new Matrix(), true, false); + } + + if (emitted.indexOf(fill) != -1) + continue; + + const path = this._pathFromShapeElements(shape); + if (!path) + continue; + + emitted.push(fill); + result.push(fill); + result.push(path); + result.push(new GraphicsEndFill()); + } + } + + private _pathFromShapeElements(shape: Shape): GraphicsPath { + const elements = shape.elements; + if (!elements || !elements.positions) + return null; + + const view = elements.positions; + const count = view.count | 0; + if (count < 3) + return null; + + const dim = view.dimensions || 2; + const stride = view.stride || dim; + const raw = view.get(count); + const verts: number[] = []; + for (let i = 0; i < count; i++) { + verts.push(raw[i * stride], raw[i * stride + 1]); + } + + const outline = this._outlineFromTriangleVerts(verts); + if (outline.length < 6) + return null; + + const commands: GraphicsPathCommand[] = [GraphicsPathCommand.MOVE_TO]; + const data: number[] = [outline[0], outline[1]]; + for (let i = 2; i + 1 < outline.length; i += 2) { + commands.push(GraphicsPathCommand.LINE_TO); + data.push(outline[i], outline[i + 1]); + } + return new GraphicsPath(commands, data); + } + private _clonePathForRead(path: GraphicsPath): GraphicsPath { const commands: GraphicsPathCommand[] = []; const data: number[] = []; @@ -2033,6 +2228,8 @@ export class Graphics extends AssetBase { if (fill.data_type == BitmapFillStyle.data_type) { const bitmap = fill; + if (!bitmap.image) + return; this.beginBitmapFill( bitmap.image, bitmap.matrix, bitmap.repeat, bitmap.smooth); } diff --git a/lib/draw/GraphicsFactoryFills.ts b/lib/draw/GraphicsFactoryFills.ts index 033c005..87fc769 100644 --- a/lib/draw/GraphicsFactoryFills.ts +++ b/lib/draw/GraphicsFactoryFills.ts @@ -95,9 +95,10 @@ export const UnpackFillStyle: Record = { [BitmapFillStyle.data_type] (style: BitmapFillStyle, data: IStyleElements): IStyleElements { - data.material = MaterialManager.getMaterialForBitmap(true); + data.material = MaterialManager.getMaterialForBitmap(true, style.image); - data.style.sampler = new ImageSampler(style.repeat, style.smooth, style.smooth); + // 3rd ImageSampler arg is mipmap, not smooth — mipmaps on NPOT fills sample black + data.style.sampler = new ImageSampler(style.repeat, style.smooth, false); data.style.image = style.image; data.style.uvMatrix = style.getUVMatrix(); diff --git a/lib/flash/StyleUtils.ts b/lib/flash/StyleUtils.ts index 7b5d62a..d3c5195 100644 --- a/lib/flash/StyleUtils.ts +++ b/lib/flash/StyleUtils.ts @@ -103,7 +103,16 @@ export class StyleUtils { } private static getImage(bitmapIndex: number, factory: IMaterialFactory): Image2D { - return factory.awaySymbols[bitmapIndex] || new BitmapImage2D(512, 512, true, 0xff0000ff, true); + const image = factory && factory.awaySymbols ? factory.awaySymbols[bitmapIndex] : null; + if (image) { + if (typeof (image as any).applySymbol === 'function') + (image as any).applySymbol(); + if (typeof (image as any).unuseWeakRef === 'function') + (image as any).unuseWeakRef(); + return image; + } + console.warn('[StyleUtils.getImage] missing bitmapId', bitmapIndex); + return new BitmapImage2D(512, 512, true, 0xff0000ff, true); } public static processMorphStyle(style: any, isLineStyle: boolean): ShapeStyle { diff --git a/lib/managers/MaterialManager.ts b/lib/managers/MaterialManager.ts index a456fd2..be7a59f 100644 --- a/lib/managers/MaterialManager.ts +++ b/lib/managers/MaterialManager.ts @@ -1,12 +1,13 @@ import { GradientFillStyle } from '../draw/fills/GradientFillStyle'; import { SolidFillStyle } from '../draw/fills/SolidFillStyle'; import { IMaterial } from '@awayjs/renderer'; -import { ImageUtils } from '@awayjs/stage'; +import { Image2D, ImageUtils } from '@awayjs/stage'; type ISpecialMaterial = IMaterial & { alphaBlending: boolean; useColorTransform: boolean; ambientMethod?: any; + animateUVs?: boolean; } type IMaterialCtr = { new(...args: any[]): ISpecialMaterial}; @@ -15,6 +16,7 @@ export class MaterialManager { private static _bitmapMaterial: ISpecialMaterial; private static _bitmapMaterialTransform: ISpecialMaterial; + private static _bitmapMaterials: Record = {}; private static _colorMaterial: ISpecialMaterial; private static _colorMaterials: any = {}; @@ -79,12 +81,26 @@ export class MaterialManager { return newmat; } - public static getMaterialForBitmap (transform: boolean = false): IMaterial { + public static getMaterialForBitmap (transform: boolean = false, image: Image2D = null): IMaterial { if (!MaterialManager.materialClass) { throw ('no materialClass registered on MaterialManager!'); } - let newmat; + let newmat: ISpecialMaterial; + + if (image) { + const key = image.id + '_' + (transform ? 't' : 'n'); + if (MaterialManager._bitmapMaterials[key]) + return MaterialManager._bitmapMaterials[key]; + + newmat = MaterialManager._bitmapMaterials[key] = new MaterialManager.materialClass(image); + if (transform) + newmat.animateUVs = true; + newmat.alphaBlending = true; + newmat.useColorTransform = true; + newmat.bothSides = true; + return newmat; + } if (transform) { From 0e8afb96cac7e6aa9ad872bcb38319286297111c Mon Sep 17 00:00:00 2001 From: Ron Carbot Date: Wed, 2 Sep 2026 20:53:32 +0000 Subject: [PATCH 3/7] Reconstruct author-time fills (not just bitmaps) in readGraphicsData. applyPattern replaces graphicsData[0]; if that slot was a stroke or empty, the clothing interiors never got the new GraphicsBitmapFill. Also clone queued ShapeTags and fall back to sourceGraphics / triangle contours. --- lib/Graphics.ts | 73 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/lib/Graphics.ts b/lib/Graphics.ts index 7e36caf..11e03c9 100644 --- a/lib/Graphics.ts +++ b/lib/Graphics.ts @@ -365,6 +365,8 @@ export class Graphics extends AssetBase { graphics._addShapes(this._shapes, cloneShapes); graphics._recorded_fill_pathes = this._recorded_fill_pathes.concat(); graphics._recorded_stroke_pathes = this._recorded_stroke_pathes.concat(); + if (this._queuedShapeTags.length) + graphics._queuedShapeTags = this._queuedShapeTags.concat(); } public clone(cloneShapes: boolean = false): Graphics { @@ -997,11 +999,22 @@ export class Graphics extends AssetBase { * (MOVE_TO, LINE_TO, CURVE_TO only). */ public readGraphicsData(): Array { + // Make sure author-time ShapeTags have been converted so recorded paths / shapes exist. + let source: Graphics = this; + if (!this._queuedShapeTags.length + && !this._shapes.length + && !this._recorded_fill_pathes.length + && this.sourceGraphics) + source = this.sourceGraphics; + + if (source._queuedShapeTags.length) + source._endFillInternal(false); + const result: IGraphicsData[] = []; const emittedFills: IFillStyle[] = []; - const fillPaths = this._collectRecordedPaths( - this._recorded_fill_pathes, this._queued_fill_pathes, this._active_fill_path); + const fillPaths = source._collectRecordedPaths( + source._recorded_fill_pathes, source._queued_fill_pathes, source._active_fill_path); for (let i = 0; i < fillPaths.length; i++) { const path = this._ensurePathCommands(fillPaths[i]); if (!this._pathHasDrawableCommands(path)) @@ -1017,10 +1030,10 @@ export class Graphics extends AssetBase { result.push(new GraphicsEndFill()); } - this._appendBitmapFillsFromShapes(result, emittedFills); + source._appendFillsFromShapes(result, emittedFills); - const strokePaths = this._collectRecordedPaths( - this._recorded_stroke_pathes, this._queued_stroke_pathes, this._active_stroke_path); + const strokePaths = source._collectRecordedPaths( + source._recorded_stroke_pathes, source._queued_stroke_pathes, source._active_stroke_path); for (let i = 0; i < strokePaths.length; i++) { const path = this._ensurePathCommands(strokePaths[i]); if (!this._pathHasDrawableCommands(path)) @@ -2055,7 +2068,7 @@ export class Graphics extends AssetBase { return out; } - private _appendBitmapFillsFromShapes(result: IGraphicsData[], emitted: IFillStyle[]): void { + private _appendFillsFromShapes(result: IGraphicsData[], emitted: IFillStyle[]): void { const shapes = this._shapes; if (!shapes) return; @@ -2066,13 +2079,22 @@ export class Graphics extends AssetBase { continue; let fill = this._unwrapFill(shape.originalFillStyle); - if (!fill || fill.data_type != BitmapFillStyle.data_type) { + // Author-time solid/gradient fills live on tessellated Shapes, not drawing-API paths. + // Previously only BitmapFillStyle was reconstructed, so graphicsData[0] was a STROKE + // (or the vector was empty) and applyPattern's `graphicsData[0] = bitmapFill` never + // replaced the interior fill. + if (!fill) { const image = shape.style && shape.style.image; if (!image) continue; fill = new BitmapFillStyle( image, new Matrix(), true, false); } + if (fill.data_type != BitmapFillStyle.data_type + && fill.data_type != SolidFillStyle.data_type + && fill.data_type != GradientFillStyle.data_type) + continue; + if (emitted.indexOf(fill) != -1) continue; @@ -2106,16 +2128,35 @@ export class Graphics extends AssetBase { } const outline = this._outlineFromTriangleVerts(verts); - if (outline.length < 6) - return null; - - const commands: GraphicsPathCommand[] = [GraphicsPathCommand.MOVE_TO]; - const data: number[] = [outline[0], outline[1]]; - for (let i = 2; i + 1 < outline.length; i += 2) { - commands.push(GraphicsPathCommand.LINE_TO); - data.push(outline[i], outline[i + 1]); + if (outline.length >= 6) { + const commands: GraphicsPathCommand[] = [GraphicsPathCommand.MOVE_TO]; + const data: number[] = [outline[0], outline[1]]; + for (let i = 2; i + 1 < outline.length; i += 2) { + commands.push(GraphicsPathCommand.LINE_TO); + data.push(outline[i], outline[i + 1]); + } + return new GraphicsPath(commands, data); } - return new GraphicsPath(commands, data); + + // Fallback: emit each triangle as a closed contour (non-zero winding) + // so applyPattern can replace the fill without losing coverage. + const commands: GraphicsPathCommand[] = []; + const data: number[] = []; + for (let i = 0; i + 5 < verts.length; i += 6) { + commands.push( + GraphicsPathCommand.MOVE_TO, + GraphicsPathCommand.LINE_TO, + GraphicsPathCommand.LINE_TO, + GraphicsPathCommand.LINE_TO); + data.push( + verts[i], verts[i + 1], + verts[i + 2], verts[i + 3], + verts[i + 4], verts[i + 5], + verts[i], verts[i + 1]); + } + if (!commands.length) + return null; + return new GraphicsPath(commands, data, GraphicsPathWinding.NON_ZERO); } private _clonePathForRead(path: GraphicsPath): GraphicsPath { From 96a3fb9805bb2bca596eeef066d5aa5f9b5c086e Mon Sep 17 00:00:00 2001 From: Ron Carbot Date: Thu, 3 Sep 2026 18:03:25 +0000 Subject: [PATCH 4/7] Emit readGraphicsData from recorded paths only. Queued and active held the same live path objects as _recorded_*, so merging them in _collectRecordedPaths was redundant and could duplicate fills. Record once by identity and skip shape-fallback bitmaps already emitted from paths. --- lib/Graphics.ts | 67 +++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/lib/Graphics.ts b/lib/Graphics.ts index 11e03c9..b3faaa6 100644 --- a/lib/Graphics.ts +++ b/lib/Graphics.ts @@ -129,6 +129,7 @@ export class Graphics extends AssetBase { private _queued_fill_pathes: GraphicsPath[] = []; private _queued_stroke_pathes: GraphicsPath[] = []; + // Survives tessellation (queued is cleared). This is what readGraphicsData emits. private _recorded_fill_pathes: GraphicsPath[] = []; private _recorded_stroke_pathes: GraphicsPath[] = []; public _active_fill_path: GraphicsPath; @@ -224,10 +225,10 @@ export class Graphics extends AssetBase { if (!isLine) { this._drawingDirty = true; this._queued_fill_pathes.push(value); - this._recordFillPath(value); + this._recordPath(this._recorded_fill_pathes, value); } else { this._queued_stroke_pathes.push(value); - this._recordStrokePath(value); + this._recordPath(this._recorded_stroke_pathes, value); if (!supressFill) { this.endFill(); @@ -1013,8 +1014,9 @@ export class Graphics extends AssetBase { const result: IGraphicsData[] = []; const emittedFills: IFillStyle[] = []; - const fillPaths = source._collectRecordedPaths( - source._recorded_fill_pathes, source._queued_fill_pathes, source._active_fill_path); + // `_recorded_*` is the single source of truth. Queued/active hold the same + // live path objects while drawing, then queued is cleared after tessellation. + const fillPaths = source._recorded_fill_pathes; for (let i = 0; i < fillPaths.length; i++) { const path = this._ensurePathCommands(fillPaths[i]); if (!this._pathHasDrawableCommands(path)) @@ -1032,8 +1034,7 @@ export class Graphics extends AssetBase { source._appendFillsFromShapes(result, emittedFills); - const strokePaths = source._collectRecordedPaths( - source._recorded_stroke_pathes, source._queued_stroke_pathes, source._active_stroke_path); + const strokePaths = source._recorded_stroke_pathes; for (let i = 0; i < strokePaths.length; i++) { const path = this._ensurePathCommands(strokePaths[i]); if (!this._pathHasDrawableCommands(path)) @@ -1904,39 +1905,10 @@ export class Graphics extends AssetBase { } - private _recordFillPath(path: GraphicsPath): void { - const recorded = this._recorded_fill_pathes; - if (recorded[recorded.length - 1] !== path) - recorded.push(path); - } - - private _recordStrokePath(path: GraphicsPath): void { - const recorded = this._recorded_stroke_pathes; - if (recorded[recorded.length - 1] !== path) - recorded.push(path); - } - - private _collectRecordedPaths( - recorded: GraphicsPath[], - queued: GraphicsPath[], - active: GraphicsPath - ): GraphicsPath[] { - const result: GraphicsPath[] = []; - const add = (list: GraphicsPath[]) => { - if (!list) - return; - for (let i = 0; i < list.length; i++) { - const path = list[i]; - if (!path || result.indexOf(path) != -1) - continue; - result.push(path); - } - }; - add(recorded); - add(queued); - if (active) - add([active]); - return result; + private _recordPath(recorded: GraphicsPath[], path: GraphicsPath): void { + if (!path || recorded.indexOf(path) != -1) + return; + recorded.push(path); } private _unwrapFill(style: IGraphicsData): IFillStyle { @@ -2087,6 +2059,19 @@ export class Graphics extends AssetBase { const image = shape.style && shape.style.image; if (!image) continue; + // Same image may already have been emitted from a recorded path + // whose BitmapFillStyle instance is not this fallback object. + let already = false; + for (let e = 0; e < emitted.length; e++) { + const other = emitted[e]; + if (other && other.data_type == BitmapFillStyle.data_type + && ( other).image === image) { + already = true; + break; + } + } + if (already) + continue; fill = new BitmapFillStyle( image, new Matrix(), true, false); } @@ -2336,7 +2321,7 @@ export class Graphics extends AssetBase { this._active_fill_path = new GraphicsPath(); this._active_fill_path.style = this._fillStyle; this._queued_fill_pathes.push(this._active_fill_path); - this._recordFillPath(this._active_fill_path); + this._recordPath(this._recorded_fill_pathes, this._active_fill_path); //auto-add move command if starting position is not zero if (this._current_position.x != 0 || this._current_position.y != 0) @@ -2353,7 +2338,7 @@ export class Graphics extends AssetBase { this._active_stroke_path = new GraphicsPath(); this._active_stroke_path.style = this._lineStyle; this._queued_stroke_pathes.push(this._active_stroke_path); - this._recordStrokePath(this._active_stroke_path); + this._recordPath(this._recorded_stroke_pathes, this._active_stroke_path); //auto-add move command if starting position is not zero if (this._current_position.x != 0 || this._current_position.y != 0) From 626b464d47001dae3796b1a2329bdde06b06bb0a Mon Sep 17 00:00:00 2001 From: Ron Carbot Date: Thu, 3 Sep 2026 23:21:29 +0000 Subject: [PATCH 5/7] Revert per-image bitmap materials; keep shared shader materials. Bitmap lives on style.image; MaterialManager only needs one transform and one non-transform bitmap material. NPOT mipmap=false sampler fix stays. --- lib/Graphics.ts | 2 +- lib/draw/GraphicsFactoryFills.ts | 2 +- lib/managers/MaterialManager.ts | 21 +++------------------ 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/Graphics.ts b/lib/Graphics.ts index b3faaa6..ac3a5b1 100644 --- a/lib/Graphics.ts +++ b/lib/Graphics.ts @@ -100,7 +100,7 @@ export class Graphics extends AssetBase { shapeStyle.smooth ); - const material = MaterialManager.getMaterialForBitmap(true, shapeStyle.image); + const material = MaterialManager.getMaterialForBitmap(true); //enforce image smooth style (mipmap=false: 3rd sampler arg is mipmap) style.sampler = new ImageSampler(shapeStyle.repeat, shapeStyle.smooth, false); diff --git a/lib/draw/GraphicsFactoryFills.ts b/lib/draw/GraphicsFactoryFills.ts index 87fc769..590de06 100644 --- a/lib/draw/GraphicsFactoryFills.ts +++ b/lib/draw/GraphicsFactoryFills.ts @@ -95,7 +95,7 @@ export const UnpackFillStyle: Record = { [BitmapFillStyle.data_type] (style: BitmapFillStyle, data: IStyleElements): IStyleElements { - data.material = MaterialManager.getMaterialForBitmap(true, style.image); + data.material = MaterialManager.getMaterialForBitmap(true); // 3rd ImageSampler arg is mipmap, not smooth — mipmaps on NPOT fills sample black data.style.sampler = new ImageSampler(style.repeat, style.smooth, false); diff --git a/lib/managers/MaterialManager.ts b/lib/managers/MaterialManager.ts index be7a59f..808468b 100644 --- a/lib/managers/MaterialManager.ts +++ b/lib/managers/MaterialManager.ts @@ -1,7 +1,7 @@ import { GradientFillStyle } from '../draw/fills/GradientFillStyle'; import { SolidFillStyle } from '../draw/fills/SolidFillStyle'; import { IMaterial } from '@awayjs/renderer'; -import { Image2D, ImageUtils } from '@awayjs/stage'; +import { ImageUtils } from '@awayjs/stage'; type ISpecialMaterial = IMaterial & { alphaBlending: boolean; @@ -16,7 +16,6 @@ export class MaterialManager { private static _bitmapMaterial: ISpecialMaterial; private static _bitmapMaterialTransform: ISpecialMaterial; - private static _bitmapMaterials: Record = {}; private static _colorMaterial: ISpecialMaterial; private static _colorMaterials: any = {}; @@ -81,29 +80,15 @@ export class MaterialManager { return newmat; } - public static getMaterialForBitmap (transform: boolean = false, image: Image2D = null): IMaterial { + public static getMaterialForBitmap (transform: boolean = false): IMaterial { if (!MaterialManager.materialClass) { throw ('no materialClass registered on MaterialManager!'); } let newmat: ISpecialMaterial; - if (image) { - const key = image.id + '_' + (transform ? 't' : 'n'); - if (MaterialManager._bitmapMaterials[key]) - return MaterialManager._bitmapMaterials[key]; - - newmat = MaterialManager._bitmapMaterials[key] = new MaterialManager.materialClass(image); - if (transform) - newmat.animateUVs = true; - newmat.alphaBlending = true; - newmat.useColorTransform = true; - newmat.bothSides = true; - return newmat; - } - + // Material holds shared shader state only; the bitmap lives on style.image. if (transform) { - if (MaterialManager._bitmapMaterialTransform) return MaterialManager._bitmapMaterialTransform; From 5dd423deb7e594ba1a682c638620b0108687f127 Mon Sep 17 00:00:00 2001 From: Ron Carbot Date: Fri, 4 Sep 2026 13:39:28 +0000 Subject: [PATCH 6/7] Drop applySymbol/unuseWeakRef from StyleUtils.getImage. Bitmap entries in awaySymbols are already Image2D; those calls were unnecessary. --- lib/flash/StyleUtils.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/flash/StyleUtils.ts b/lib/flash/StyleUtils.ts index d3c5195..70d77b1 100644 --- a/lib/flash/StyleUtils.ts +++ b/lib/flash/StyleUtils.ts @@ -103,14 +103,10 @@ export class StyleUtils { } private static getImage(bitmapIndex: number, factory: IMaterialFactory): Image2D { - const image = factory && factory.awaySymbols ? factory.awaySymbols[bitmapIndex] : null; - if (image) { - if (typeof (image as any).applySymbol === 'function') - (image as any).applySymbol(); - if (typeof (image as any).unuseWeakRef === 'function') - (image as any).unuseWeakRef(); + // awaySymbols[bitmapId] is already the Image2D for DefineBits*; no symbol apply needed. + const image = factory?.awaySymbols?.[bitmapIndex]; + if (image) return image; - } console.warn('[StyleUtils.getImage] missing bitmapId', bitmapIndex); return new BitmapImage2D(512, 512, true, 0xff0000ff, true); } From 923d6301c6871997132d8e43cc5786ea3cfec103 Mon Sep 17 00:00:00 2001 From: Rob Bateman Date: Fri, 4 Sep 2026 15:03:29 +0100 Subject: [PATCH 7/7] remove uncessary over-fitting updates --- lib/Graphics.ts | 14 +++----------- lib/draw/GraphicsFactoryFills.ts | 3 +-- lib/flash/StyleUtils.ts | 7 +------ 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/lib/Graphics.ts b/lib/Graphics.ts index ac3a5b1..639bc22 100644 --- a/lib/Graphics.ts +++ b/lib/Graphics.ts @@ -102,14 +102,12 @@ export class Graphics extends AssetBase { const material = MaterialManager.getMaterialForBitmap(true); - //enforce image smooth style (mipmap=false: 3rd sampler arg is mipmap) - style.sampler = new ImageSampler(shapeStyle.repeat, shapeStyle.smooth, false); + //enforce image smooth style + style.sampler = new ImageSampler(shapeStyle.repeat, shapeStyle.smooth, shapeStyle.smooth); style.uvMatrix = bitmapFillStyle.getUVMatrix(); - const shape = Shape.getShape(element, material, style); - shape.originalFillStyle = bitmapFillStyle; - return shape; + return Shape.getShape(element, material, style); } public static getGraphics(): Graphics { @@ -591,11 +589,6 @@ export class Graphics extends AssetBase { if (this._fillStyle) this.endFill(); - if (!bitmap) { - console.warn('[beginBitmapFill] null bitmap'); - return; - } - if (!this._bitmapFillPool) { this._bitmapFillPool = {}; } @@ -611,7 +604,6 @@ export class Graphics extends AssetBase { smooth) ); } else { - fill.fillStyle.image = bitmap; fill.fillStyle.matrix = matrix; fill.fillStyle.repeat = repeat; fill.fillStyle.smooth = smooth; diff --git a/lib/draw/GraphicsFactoryFills.ts b/lib/draw/GraphicsFactoryFills.ts index 590de06..033c005 100644 --- a/lib/draw/GraphicsFactoryFills.ts +++ b/lib/draw/GraphicsFactoryFills.ts @@ -97,8 +97,7 @@ export const UnpackFillStyle: Record = { data.material = MaterialManager.getMaterialForBitmap(true); - // 3rd ImageSampler arg is mipmap, not smooth — mipmaps on NPOT fills sample black - data.style.sampler = new ImageSampler(style.repeat, style.smooth, false); + data.style.sampler = new ImageSampler(style.repeat, style.smooth, style.smooth); data.style.image = style.image; data.style.uvMatrix = style.getUVMatrix(); diff --git a/lib/flash/StyleUtils.ts b/lib/flash/StyleUtils.ts index 70d77b1..7b5d62a 100644 --- a/lib/flash/StyleUtils.ts +++ b/lib/flash/StyleUtils.ts @@ -103,12 +103,7 @@ export class StyleUtils { } private static getImage(bitmapIndex: number, factory: IMaterialFactory): Image2D { - // awaySymbols[bitmapId] is already the Image2D for DefineBits*; no symbol apply needed. - const image = factory?.awaySymbols?.[bitmapIndex]; - if (image) - return image; - console.warn('[StyleUtils.getImage] missing bitmapId', bitmapIndex); - return new BitmapImage2D(512, 512, true, 0xff0000ff, true); + return factory.awaySymbols[bitmapIndex] || new BitmapImage2D(512, 512, true, 0xff0000ff, true); } public static processMorphStyle(style: any, isLineStyle: boolean): ShapeStyle {